├── .gitignore ├── tests ├── View │ ├── fixtures │ │ ├── basic.php │ │ ├── nested │ │ │ ├── basic.php │ │ │ └── child.php │ │ ├── namespaced │ │ │ └── basic.php │ │ ├── section-exception-layout.php │ │ ├── component.php │ │ └── section-exception.php │ ├── DataObjectStub.php │ ├── Blade │ │ ├── BladeShowTest.php │ │ ├── BladeAppendTest.php │ │ ├── BladeStopSectionTest.php │ │ ├── BladeEndSectionsTest.php │ │ ├── BladeOverwriteSectionTest.php │ │ ├── BladeStackTest.php │ │ ├── BladeUnsetStatementsTest.php │ │ ├── BladePushTest.php │ │ ├── BladeIfEmptyStatementsTest.php │ │ ├── BladeIfIssetStatementsTest.php │ │ ├── BladeSectionTest.php │ │ ├── BladeEachTest.php │ │ ├── BladePrependTest.php │ │ ├── BladeUnlessStatementsTest.php │ │ ├── BladeHasSectionTest.php │ │ ├── BladeElseIfStatementsTest.php │ │ ├── BladeYieldTest.php │ │ ├── BladeIncludeTest.php │ │ ├── BladeCanStatementsTest.php │ │ ├── BladeIncludeIfTest.php │ │ ├── BladeIncludeWhenTest.php │ │ ├── BladeIncludeFirstTest.php │ │ ├── BladeCannotStatementsTest.php │ │ ├── AbstractBladeTestCase.php │ │ ├── BladeHelpersTest.php │ │ ├── BladeCananyStatementsTest.php │ │ ├── BladeJsonTest.php │ │ ├── BladeEscapedTest.php │ │ ├── BladeLangTest.php │ │ ├── BladeWhileStatementsTest.php │ │ ├── BladeCommentsTest.php │ │ ├── BladeIfGuestStatementsTest.php │ │ ├── BladeElseStatementsTest.php │ │ ├── BladeForStatementsTest.php │ │ ├── BladeExpressionTest.php │ │ ├── BladeElseGuestStatementsTest.php │ │ ├── BladeIfStatementsTest.php │ │ ├── BladeIfAuthStatementsTest.php │ │ ├── BladeElseAuthStatementsTest.php │ │ ├── BladeExtendsTest.php │ │ ├── BladePhpStatementsTest.php │ │ ├── BladeBreakStatementsTest.php │ │ ├── BladeContinueStatementsTest.php │ │ ├── BladeVerbatimTest.php │ │ ├── BladeForelseStatementsTest.php │ │ ├── BladeEchoTest.php │ │ ├── BladeForeachStatementsTest.php │ │ └── BladeCustomTest.php │ ├── ViewCompilerEngineTest.php │ ├── ViewStringBladeCompilerTest.php │ ├── ViewTest.php │ ├── StringViewTest.php │ └── ViewFactoryTest.php └── bootstrap.php ├── config └── blade.php ├── composer.json ├── LICENSE ├── phpunit.xml ├── src ├── Facades │ └── StringBlade.php ├── View.php ├── Engines │ └── CompilerEngine.php ├── Compilers │ ├── BladeCompiler.php │ └── StringBladeCompiler.php ├── StringView.php ├── Factory.php └── StringBladeServiceProvider.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.netbeans/ 2 | /bkp/ 3 | vendor/ -------------------------------------------------------------------------------- /tests/View/fixtures/basic.php: -------------------------------------------------------------------------------- 1 | 2 | Hello World 3 | -------------------------------------------------------------------------------- /tests/View/fixtures/nested/basic.php: -------------------------------------------------------------------------------- 1 | Hello World 2 | -------------------------------------------------------------------------------- /tests/View/fixtures/namespaced/basic.php: -------------------------------------------------------------------------------- 1 | Hello World 2 | -------------------------------------------------------------------------------- /tests/View/fixtures/nested/child.php: -------------------------------------------------------------------------------- 1 | Hello World 2 | -------------------------------------------------------------------------------- /tests/View/fixtures/section-exception-layout.php: -------------------------------------------------------------------------------- 1 | yieldContent('content'); ?> 2 | -------------------------------------------------------------------------------- /tests/View/fixtures/component.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/View/DataObjectStub.php: -------------------------------------------------------------------------------- 1 | make('layout', \Illuminate\Support\Arr::except(get_defined_vars(), ['__data', '__path']))->render(); ?> 2 | startSection('content'); ?> 3 | 4 | stopSection(); ?> 5 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeShowTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('yieldSection(); ?>', $this->compiler->compileString('@show')); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeAppendTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('appendSection(); ?>', $this->compiler->compileString('@append')); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeStopSectionTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('stopSection(); ?>', $this->compiler->compileString('@stop')); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeEndSectionsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('stopSection(); ?>', $this->compiler->compileString('@endsection')); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeOverwriteSectionTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('stopSection(true); ?>', $this->compiler->compileString('@overwrite')); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeStackTest.php: -------------------------------------------------------------------------------- 1 | yieldPushContent(\'foo\'); ?>'; 11 | $this->assertEquals($expected, $this->compiler->compileString($string)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeUnsetStatementsTest.php: -------------------------------------------------------------------------------- 1 | '; 11 | $this->assertEquals($expected, $this->compiler->compileString($string)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /config/blade.php: -------------------------------------------------------------------------------- 1 | env('STRING_BLADE_CACHE_TIMEOUT', 300), 8 | 9 | // Determine whether the service provider to autoload blade custom directives. 10 | 'autoload_custom_directives' => env('STRING_BLADE_AUTOLOAD', false), 11 | ]; -------------------------------------------------------------------------------- /tests/View/Blade/BladePushTest.php: -------------------------------------------------------------------------------- 1 | startPush(\'foo\'); ?> 13 | test 14 | stopPush(); ?>'; 15 | $this->assertEquals($expected, $this->compiler->compileString($string)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeIfEmptyStatementsTest.php: -------------------------------------------------------------------------------- 1 | 13 | breeze 14 | '; 15 | $this->assertEquals($expected, $this->compiler->compileString($string)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeIfIssetStatementsTest.php: -------------------------------------------------------------------------------- 1 | 13 | breeze 14 | '; 15 | $this->assertEquals($expected, $this->compiler->compileString($string)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeSectionTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('startSection(\'foo\'); ?>', $this->compiler->compileString('@section(\'foo\')')); 10 | $this->assertEquals('startSection(name(foo)); ?>', $this->compiler->compileString('@section(name(foo))')); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeEachTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('renderEach(\'foo\', \'bar\'); ?>', $this->compiler->compileString('@each(\'foo\', \'bar\')')); 10 | $this->assertEquals('renderEach(name(foo)); ?>', $this->compiler->compileString('@each(name(foo))')); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/View/Blade/BladePrependTest.php: -------------------------------------------------------------------------------- 1 | startPrepend(\'foo\'); ?> 13 | bar 14 | stopPrepend(); ?>'; 15 | 16 | $this->assertEquals($expected, $this->compiler->compileString($string)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeUnlessStatementsTest.php: -------------------------------------------------------------------------------- 1 | 13 | breeze 14 | '; 15 | $this->assertEquals($expected, $this->compiler->compileString($string)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeHasSectionTest.php: -------------------------------------------------------------------------------- 1 | yieldContent("section")))): ?> 13 | breeze 14 | '; 15 | $this->assertEquals($expected, $this->compiler->compileString($string)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeElseIfStatementsTest.php: -------------------------------------------------------------------------------- 1 | 15 | breeze 16 | 17 | boom 18 | '; 19 | $this->assertEquals($expected, $this->compiler->compileString($string)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeYieldTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('yieldContent(\'foo\'); ?>', $this->compiler->compileString('@yield(\'foo\')')); 10 | $this->assertEquals('yieldContent(\'foo\', \'bar\'); ?>', $this->compiler->compileString('@yield(\'foo\', \'bar\')')); 11 | $this->assertEquals('yieldContent(name(foo)); ?>', $this->compiler->compileString('@yield(name(foo))')); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeIncludeTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('make(\'foo\', \Illuminate\Support\Arr::except(get_defined_vars(), [\'__data\', \'__path\']))->render(); ?>', $this->compiler->compileString('@include(\'foo\')')); 10 | $this->assertEquals('make(name(foo), \Illuminate\Support\Arr::except(get_defined_vars(), [\'__data\', \'__path\']))->render(); ?>', $this->compiler->compileString('@include(name(foo))')); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeCanStatementsTest.php: -------------------------------------------------------------------------------- 1 | check(\'update\', [$post])): ?> 15 | breeze 16 | check(\'delete\', [$post])): ?> 17 | sneeze 18 | '; 19 | $this->assertEquals($expected, $this->compiler->compileString($string)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeIncludeIfTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('exists(\'foo\')) echo $__env->make(\'foo\', \Illuminate\Support\Arr::except(get_defined_vars(), [\'__data\', \'__path\']))->render(); ?>', $this->compiler->compileString('@includeIf(\'foo\')')); 10 | $this->assertEquals('exists(name(foo))) echo $__env->make(name(foo), \Illuminate\Support\Arr::except(get_defined_vars(), [\'__data\', \'__path\']))->render(); ?>', $this->compiler->compileString('@includeIf(name(foo))')); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeIncludeWhenTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('renderWhen(true, \'foo\', ["foo" => "bar"], \Illuminate\Support\Arr::except(get_defined_vars(), [\'__data\', \'__path\'])); ?>', $this->compiler->compileString('@includeWhen(true, \'foo\', ["foo" => "bar"])')); 10 | $this->assertEquals('renderWhen(true, \'foo\', \Illuminate\Support\Arr::except(get_defined_vars(), [\'__data\', \'__path\'])); ?>', $this->compiler->compileString('@includeWhen(true, \'foo\')')); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeIncludeFirstTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('first(["one", "two"], \Illuminate\Support\Arr::except(get_defined_vars(), [\'__data\', \'__path\']))->render(); ?>', $this->compiler->compileString('@includeFirst(["one", "two"])')); 10 | $this->assertEquals('first(["one", "two"], ["foo" => "bar"], \Illuminate\Support\Arr::except(get_defined_vars(), [\'__data\', \'__path\']))->render(); ?>', $this->compiler->compileString('@includeFirst(["one", "two"], ["foo" => "bar"])')); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeCannotStatementsTest.php: -------------------------------------------------------------------------------- 1 | denies(\'update\', [$post])): ?> 15 | breeze 16 | denies(\'delete\', [$post])): ?> 17 | sneeze 18 | '; 19 | $this->assertEquals($expected, $this->compiler->compileString($string)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/View/Blade/AbstractBladeTestCase.php: -------------------------------------------------------------------------------- 1 | compiler = new BladeCompiler(m::mock(Filesystem::class), __DIR__); 18 | parent::setUp(); 19 | } 20 | 21 | protected function tearDown(): void 22 | { 23 | m::close(); 24 | 25 | parent::tearDown(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeHelpersTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('', $this->compiler->compileString('@csrf')); 10 | $this->assertEquals('', $this->compiler->compileString("@method('patch')")); 11 | $this->assertEquals('', $this->compiler->compileString('@dd($var1)')); 12 | $this->assertEquals('', $this->compiler->compileString('@dd($var1, $var2)')); 13 | $this->assertEquals('', $this->compiler->compileString('@dump($var1, $var2)')); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeCananyStatementsTest.php: -------------------------------------------------------------------------------- 1 | any([\'create\', \'update\'], [$post])): ?> 15 | breeze 16 | any([\'delete\', \'approve\'], [$post])): ?> 17 | sneeze 18 | '; 19 | $this->assertEquals($expected, $this->compiler->compileString($string)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeJsonTest.php: -------------------------------------------------------------------------------- 1 | ;'; 11 | 12 | $this->assertEquals($expected, $this->compiler->compileString($string)); 13 | } 14 | 15 | public function testEncodingOptionsCanBeOverwritten() 16 | { 17 | $string = 'var foo = @json($var, JSON_HEX_TAG);'; 18 | $expected = 'var foo = ;'; 19 | 20 | $this->assertEquals($expected, $this->compiler->compileString($string)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeEscapedTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('@foreach', $this->compiler->compileString('@@foreach')); 10 | $this->assertEquals('@verbatim @continue @endverbatim', $this->compiler->compileString('@@verbatim @@continue @@endverbatim')); 11 | $this->assertEquals('@foreach($i as $x)', $this->compiler->compileString('@@foreach($i as $x)')); 12 | $this->assertEquals('@continue @break', $this->compiler->compileString('@@continue @@break')); 13 | $this->assertEquals('@foreach( 14 | $i as $x 15 | )', $this->compiler->compileString('@@foreach( 16 | $i as $x 17 | )')); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeLangTest.php: -------------------------------------------------------------------------------- 1 | getFromJson(function_call('foo(blah)')); ?> bar"; 11 | $this->assertEquals($expected, $this->compiler->compileString($string)); 12 | } 13 | 14 | public function testLanguageAndChoicesAreCompiled() 15 | { 16 | $this->assertEquals('getFromJson(\'foo\'); ?>', $this->compiler->compileString("@lang('foo')")); 17 | $this->assertEquals('choice(\'foo\', 1); ?>', $this->compiler->compileString("@choice('foo', 1)")); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeWhileStatementsTest.php: -------------------------------------------------------------------------------- 1 | 13 | test 14 | '; 15 | $this->assertEquals($expected, $this->compiler->compileString($string)); 16 | } 17 | 18 | public function testNestedWhileStatementsAreCompiled() 19 | { 20 | $string = '@while ($foo) 21 | @while ($bar) 22 | test 23 | @endwhile 24 | @endwhile'; 25 | $expected = ' 26 | 27 | test 28 | 29 | '; 30 | $this->assertEquals($expected, $this->compiler->compileString($string)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeCommentsTest.php: -------------------------------------------------------------------------------- 1 | assertEmpty($this->compiler->compileString($string)); 11 | 12 | $string = '{{-- 13 | this is a comment 14 | --}}'; 15 | $this->assertEmpty($this->compiler->compileString($string)); 16 | 17 | $string = sprintf('{{-- this is an %s long comment --}}', str_repeat('extremely ', 1000)); 18 | $this->assertEmpty($this->compiler->compileString($string)); 19 | } 20 | 21 | public function testBladeCodeInsideCommentsIsNotCompiled() 22 | { 23 | $string = '{{-- @foreach() --}}'; 24 | 25 | $this->assertEmpty($this->compiler->compileString($string)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeIfGuestStatementsTest.php: -------------------------------------------------------------------------------- 1 | getFiles(), __DIR__); 20 | $string = '@guest("api") 21 | breeze 22 | @endguest'; 23 | $expected = 'guard("api")->guest()): ?> 24 | breeze 25 | '; 26 | $this->assertEquals($expected, $compiler->compileString($string)); 27 | } 28 | 29 | protected function getFiles() 30 | { 31 | return m::mock(Filesystem::class); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeElseStatementsTest.php: -------------------------------------------------------------------------------- 1 | 15 | breeze 16 | 17 | boom 18 | '; 19 | $this->assertEquals($expected, $this->compiler->compileString($string)); 20 | } 21 | 22 | public function testElseIfStatementsAreCompiled() 23 | { 24 | $string = '@if(name(foo(bar))) 25 | breeze 26 | @elseif(boom(breeze)) 27 | boom 28 | @endif'; 29 | $expected = ' 30 | breeze 31 | 32 | boom 33 | '; 34 | $this->assertEquals($expected, $this->compiler->compileString($string)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeForStatementsTest.php: -------------------------------------------------------------------------------- 1 | 13 | test 14 | '; 15 | $this->assertEquals($expected, $this->compiler->compileString($string)); 16 | } 17 | 18 | public function testNestedForStatementsAreCompiled() 19 | { 20 | $string = '@for ($i = 0; $i < 10; $i++) 21 | @for ($j = 0; $j < 20; $j++) 22 | test 23 | @endfor 24 | @endfor'; 25 | $expected = ' 26 | 27 | test 28 | 29 | '; 30 | $this->assertEquals($expected, $this->compiler->compileString($string)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wpb/string-blade-compiler", 3 | "description": "Laravel Blade String Compiler, render string as blade templates", 4 | "keywords": ["laravel", "blade", "compiler"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Terre Porter", 9 | "email": "tporter@webpage-builders.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^7.2|^8.0", 14 | "laravel/framework": "^6.0|^7.0|^8.0|^9.0|^10.0" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "Wpb\\String_Blade_Compiler\\": "src/" 19 | } 20 | }, 21 | "minimum-stability": "dev", 22 | "extra": { 23 | "laravel": { 24 | "providers": [ 25 | "Wpb\\String_Blade_Compiler\\StringBladeServiceProvider" 26 | ], 27 | "aliases": { 28 | "StringBlade": "Wpb\\String_Blade_Compiler\\Facades\\StringBlade" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeExpressionTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('getFromJson(foo(bar(baz(qux(breeze()))))); ?> space () getFromJson(foo(bar)); ?>', $this->compiler->compileString('@lang(foo(bar(baz(qux(breeze()))))) space () @lang(foo(bar))')); 10 | } 11 | 12 | public function testExpressionWithinHTML() 13 | { 14 | $this->assertEquals('>', $this->compiler->compileString('')); 15 | $this->assertEquals('>', $this->compiler->compileString('')); 16 | $this->assertEquals(' getFromJson(\'foo\'); ?>>', $this->compiler->compileString('')); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeElseGuestStatementsTest.php: -------------------------------------------------------------------------------- 1 | getFiles(), __DIR__); 20 | $string = '@guest("api") 21 | breeze 22 | @elseguest("standard") 23 | wheeze 24 | @endguest'; 25 | $expected = 'guard("api")->guest()): ?> 26 | breeze 27 | guard("standard")->guest()): ?> 28 | wheeze 29 | '; 30 | $this->assertEquals($expected, $compiler->compileString($string)); 31 | } 32 | 33 | protected function getFiles() 34 | { 35 | return m::mock(Filesystem::class); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Terre Porter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | ./tests 18 | 19 | 20 | 21 | 22 | ./src 23 | 24 | ./src/ 25 | 26 | 27 | 28 | 29 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeIfStatementsTest.php: -------------------------------------------------------------------------------- 1 | 13 | breeze 14 | '; 15 | $this->assertEquals($expected, $this->compiler->compileString($string)); 16 | } 17 | 18 | public function testSwitchstatementsAreCompiled() 19 | { 20 | $string = '@switch(true) 21 | @case(1) 22 | foo 23 | 24 | @case(2) 25 | bar 26 | @endswitch 27 | 28 | foo 29 | 30 | @switch(true) 31 | @case(1) 32 | foo 33 | 34 | @case(2) 35 | bar 36 | @endswitch'; 37 | $expected = ' 39 | foo 40 | 41 | 42 | bar 43 | 44 | 45 | foo 46 | 47 | 49 | foo 50 | 51 | 52 | bar 53 | '; 54 | $this->assertEquals($expected, $this->compiler->compileString($string)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | getFiles(), __DIR__); 20 | $string = '@auth("api") 21 | breeze 22 | @endauth'; 23 | $expected = 'guard("api")->check()): ?> 24 | breeze 25 | '; 26 | $this->assertEquals($expected, $compiler->compileString($string)); 27 | } 28 | 29 | public function testPlainIfStatementsAreCompiled() 30 | { 31 | $compiler = new BladeCompiler($this->getFiles(), __DIR__); 32 | $string = '@auth 33 | breeze 34 | @endauth'; 35 | $expected = 'guard()->check()): ?> 36 | breeze 37 | '; 38 | $this->assertEquals($expected, $compiler->compileString($string)); 39 | } 40 | 41 | protected function getFiles() 42 | { 43 | return m::mock(Filesystem::class); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Facades/StringBlade.php: -------------------------------------------------------------------------------- 1 | getEngineResolver()->resolve('stringblade')->getCompiler(); 30 | //return static::$app['view']->getEngineResolver()->resolve('stringblade')->getCompiler(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeElseAuthStatementsTest.php: -------------------------------------------------------------------------------- 1 | getFiles(), __DIR__); 20 | $string = '@auth("api") 21 | breeze 22 | @elseauth("standard") 23 | wheeze 24 | @endauth'; 25 | $expected = 'guard("api")->check()): ?> 26 | breeze 27 | guard("standard")->check()): ?> 28 | wheeze 29 | '; 30 | $this->assertEquals($expected, $compiler->compileString($string)); 31 | } 32 | 33 | public function testPlainElseAuthStatementsAreCompiled() 34 | { 35 | $compiler = new BladeCompiler($this->getFiles(), __DIR__); 36 | $string = '@auth("api") 37 | breeze 38 | @elseauth 39 | wheeze 40 | @endauth'; 41 | $expected = 'guard("api")->check()): ?> 42 | breeze 43 | guard()->check()): ?> 44 | wheeze 45 | '; 46 | $this->assertEquals($expected, $compiler->compileString($string)); 47 | } 48 | 49 | protected function getFiles() 50 | { 51 | return m::mock(Filesystem::class); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/View.php: -------------------------------------------------------------------------------- 1 | view = $view; 33 | $this->path = $path; 34 | $this->engine = $engine; 35 | $this->factory = $factory; 36 | 37 | $this->data = $data instanceof Arrayable ? $data->toArray() : (array) $data; 38 | 39 | } 40 | 41 | /** 42 | * Dynamically bind parameters to the view. 43 | * 44 | * @param string $method 45 | * @param array $parameters 46 | * @return \Illuminate\View\View 47 | * 48 | * @throws \BadMethodCallException 49 | */ 50 | public function __call($method, $parameters) 51 | { 52 | return parent::__call($method, $parameters); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeExtendsTest.php: -------------------------------------------------------------------------------- 1 | make(\'foo\', \Illuminate\Support\Arr::except(get_defined_vars(), [\'__data\', \'__path\']))->render(); ?>'; 12 | $this->assertEquals($expected, $this->compiler->compileString($string)); 13 | 14 | $string = '@extends(name(foo))'.PHP_EOL.'test'; 15 | $expected = 'test'.PHP_EOL.'make(name(foo), \Illuminate\Support\Arr::except(get_defined_vars(), [\'__data\', \'__path\']))->render(); ?>'; 16 | $this->assertEquals($expected, $this->compiler->compileString($string)); 17 | } 18 | 19 | public function testSequentialCompileStringCalls() 20 | { 21 | $string = '@extends(\'foo\') 22 | test'; 23 | $expected = 'test'.PHP_EOL.'make(\'foo\', \Illuminate\Support\Arr::except(get_defined_vars(), [\'__data\', \'__path\']))->render(); ?>'; 24 | $this->assertEquals($expected, $this->compiler->compileString($string)); 25 | 26 | // use the same compiler instance to compile another template with @extends directive 27 | $string = '@extends(name(foo))'.PHP_EOL.'test'; 28 | $expected = 'test'.PHP_EOL.'make(name(foo), \Illuminate\Support\Arr::except(get_defined_vars(), [\'__data\', \'__path\']))->render(); ?>'; 29 | $this->assertEquals($expected, $this->compiler->compileString($string)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/View/ViewCompilerEngineTest.php: -------------------------------------------------------------------------------- 1 | getEngine(); 21 | $engine->getCompiler()->shouldReceive('getCompiledPath')->with(__DIR__.'/fixtures/foo.php')->andReturn(__DIR__.'/fixtures/basic.php'); 22 | $engine->getCompiler()->shouldReceive('isExpired')->once()->with(__DIR__.'/fixtures/foo.php')->andReturn(true); 23 | $engine->getCompiler()->shouldReceive('compile')->once()->with(__DIR__.'/fixtures/foo.php'); 24 | $results = $engine->get(__DIR__.'/fixtures/foo.php'); 25 | 26 | $this->assertEquals('Hello World 27 | ', $results); 28 | } 29 | 30 | public function testViewsAreNotRecompiledIfTheyAreNotExpired() 31 | { 32 | $engine = $this->getEngine(); 33 | $engine->getCompiler()->shouldReceive('getCompiledPath')->with(__DIR__.'/fixtures/foo.php')->andReturn(__DIR__.'/fixtures/basic.php'); 34 | $engine->getCompiler()->shouldReceive('isExpired')->once()->andReturn(false); 35 | $engine->getCompiler()->shouldReceive('compile')->never(); 36 | $results = $engine->get(__DIR__.'/fixtures/foo.php'); 37 | 38 | $this->assertEquals('Hello World 39 | ', $results); 40 | } 41 | 42 | protected function getEngine() 43 | { 44 | return new CompilerEngine(m::mock(CompilerInterface::class)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/View/Blade/BladePhpStatementsTest.php: -------------------------------------------------------------------------------- 1 | '; 11 | $this->assertEquals($expected, $this->compiler->compileString($string)); 12 | } 13 | 14 | public function testPhpStatementsWithoutExpressionAreIgnored() 15 | { 16 | $string = '@php'; 17 | $expected = '@php'; 18 | $this->assertEquals($expected, $this->compiler->compileString($string)); 19 | 20 | $string = '{{ "Ignore: @php" }}'; 21 | $expected = ''; 22 | $this->assertEquals($expected, $this->compiler->compileString($string)); 23 | } 24 | 25 | public function testPhpStatementsDontParseBladeCode() 26 | { 27 | $string = '@php echo "{{ This is a blade tag }}" @endphp'; 28 | $expected = ''; 29 | $this->assertEquals($expected, $this->compiler->compileString($string)); 30 | } 31 | 32 | public function testVerbatimAndPhpStatementsDontGetMixedUp() 33 | { 34 | $string = "@verbatim {{ Hello, I'm not blade! }}" 35 | ."\n@php echo 'And I'm not PHP!' @endphp" 36 | ."\n@endverbatim {{ 'I am Blade' }}" 37 | ."\n@php echo 'I am PHP {{ not Blade }}' @endphp"; 38 | 39 | $expected = " {{ Hello, I'm not blade! }}" 40 | ."\n@php echo 'And I'm not PHP!' @endphp" 41 | ."\n " 42 | ."\n\n"; 43 | 44 | $this->assertEquals($expected, $this->compiler->compileString($string)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeBreakStatementsTest.php: -------------------------------------------------------------------------------- 1 | 14 | test 15 | 16 | '; 17 | $this->assertEquals($expected, $this->compiler->compileString($string)); 18 | } 19 | 20 | public function testBreakStatementsWithExpressionAreCompiled() 21 | { 22 | $string = '@for ($i = 0; $i < 10; $i++) 23 | test 24 | @break(TRUE) 25 | @endfor'; 26 | $expected = ' 27 | test 28 | 29 | '; 30 | $this->assertEquals($expected, $this->compiler->compileString($string)); 31 | } 32 | 33 | public function testBreakStatementsWithArgumentAreCompiled() 34 | { 35 | $string = '@for ($i = 0; $i < 10; $i++) 36 | test 37 | @break(2) 38 | @endfor'; 39 | $expected = ' 40 | test 41 | 42 | '; 43 | $this->assertEquals($expected, $this->compiler->compileString($string)); 44 | } 45 | 46 | public function testBreakStatementsWithSpacedArgumentAreCompiled() 47 | { 48 | $string = '@for ($i = 0; $i < 10; $i++) 49 | test 50 | @break( 2 ) 51 | @endfor'; 52 | $expected = ' 53 | test 54 | 55 | '; 56 | $this->assertEquals($expected, $this->compiler->compileString($string)); 57 | } 58 | 59 | public function testBreakStatementsWithFaultyArgumentAreCompiled() 60 | { 61 | $string = '@for ($i = 0; $i < 10; $i++) 62 | test 63 | @break(-2) 64 | @endfor'; 65 | $expected = ' 66 | test 67 | 68 | '; 69 | $this->assertEquals($expected, $this->compiler->compileString($string)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeContinueStatementsTest.php: -------------------------------------------------------------------------------- 1 | 14 | test 15 | 16 | '; 17 | $this->assertEquals($expected, $this->compiler->compileString($string)); 18 | } 19 | 20 | public function testContinueStatementsWithExpressionAreCompiled() 21 | { 22 | $string = '@for ($i = 0; $i < 10; $i++) 23 | test 24 | @continue(TRUE) 25 | @endfor'; 26 | $expected = ' 27 | test 28 | 29 | '; 30 | $this->assertEquals($expected, $this->compiler->compileString($string)); 31 | } 32 | 33 | public function testContinueStatementsWithArgumentAreCompiled() 34 | { 35 | $string = '@for ($i = 0; $i < 10; $i++) 36 | test 37 | @continue(2) 38 | @endfor'; 39 | $expected = ' 40 | test 41 | 42 | '; 43 | $this->assertEquals($expected, $this->compiler->compileString($string)); 44 | } 45 | 46 | public function testContinueStatementsWithSpacedArgumentAreCompiled() 47 | { 48 | $string = '@for ($i = 0; $i < 10; $i++) 49 | test 50 | @continue( 2 ) 51 | @endfor'; 52 | $expected = ' 53 | test 54 | 55 | '; 56 | $this->assertEquals($expected, $this->compiler->compileString($string)); 57 | } 58 | 59 | public function testContinueStatementsWithFaultyArgumentAreCompiled() 60 | { 61 | $string = '@for ($i = 0; $i < 10; $i++) 62 | test 63 | @continue(-2) 64 | @endfor'; 65 | $expected = ' 66 | test 67 | 68 | '; 69 | $this->assertEquals($expected, $this->compiler->compileString($string)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Engines/CompilerEngine.php: -------------------------------------------------------------------------------- 1 | lastCompiled[] = $path; 29 | } else { 30 | $this->lastCompiled[] = 'StringView-'.$path->cache_key; 31 | } 32 | 33 | // If this given view has expired, which means it has simply been edited since 34 | // it was last compiled, we will re-compile the views so we can evaluate a 35 | // fresh copy of the view. We'll pass the compiler the path of the view. 36 | if ($this->compiler->isExpired($path)) { 37 | $this->compiler->compile($path); 38 | } 39 | 40 | $compiled = $this->compiler->getCompiledPath($path); 41 | 42 | // Once we have the path to the compiled file, we will evaluate the paths with 43 | // typical PHP just like any other templates. We also keep a stack of views 44 | // which have been rendered for right exception messages to be generated. 45 | $results = $this->evaluatePath($compiled, $data); 46 | 47 | array_pop($this->lastCompiled); 48 | 49 | return $results; 50 | } 51 | 52 | /** 53 | * Set the delete view cache after render flag. 54 | * 55 | * @deprecated I can't seem to find when the setting was actually used. If you want this, submit a bug and I will see about adding the ability. 56 | * 57 | * @param bool $delete 58 | */ 59 | public function setDeleteViewCacheAfterRender($delete = true) { 60 | $this->deleteViewCacheAfterRender = $delete; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeVerbatimTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expected, $this->compiler->compileString($string)); 12 | } 13 | 14 | public function testVerbatimBlocksWithMultipleLinesAreCompiled() 15 | { 16 | $string = 'Some text 17 | @verbatim 18 | {{ $a }} 19 | @if($b) 20 | {{ $b }} 21 | @endif 22 | @endverbatim'; 23 | $expected = 'Some text 24 | 25 | {{ $a }} 26 | @if($b) 27 | {{ $b }} 28 | @endif 29 | '; 30 | $this->assertEquals($expected, $this->compiler->compileString($string)); 31 | } 32 | 33 | public function testMultipleVerbatimBlocksAreCompiled() 34 | { 35 | $string = '@verbatim {{ $a }} @endverbatim {{ $b }} @verbatim {{ $c }} @endverbatim'; 36 | $expected = ' {{ $a }} {{ $c }} '; 37 | $this->assertEquals($expected, $this->compiler->compileString($string)); 38 | } 39 | 40 | public function testRawBlocksAreRenderedInTheRightOrder() 41 | { 42 | $string = '@php echo "#1"; @endphp @verbatim {{ #2 }} @endverbatim @verbatim {{ #3 }} @endverbatim @php echo "#4"; @endphp'; 43 | 44 | $expected = ' {{ #2 }} {{ #3 }} '; 45 | 46 | $this->assertSame($expected, $this->compiler->compileString($string)); 47 | } 48 | 49 | public function testMultilineTemplatesWithRawBlocksAreRenderedInTheRightOrder() 50 | { 51 | $string = '{{ $first }} 52 | @php 53 | echo $second; 54 | @endphp 55 | @if ($conditional) 56 | {{ $third }} 57 | @endif 58 | @include("users") 59 | @verbatim 60 | {{ $fourth }} @include("test") 61 | @endverbatim 62 | @php echo $fifth; @endphp'; 63 | 64 | $expected = ' 65 | 66 | 69 | 70 | 71 | 72 | 73 | make("users", \Illuminate\Support\Arr::except(get_defined_vars(), [\'__data\', \'__path\']))->render(); ?> 74 | 75 | {{ $fourth }} @include("test") 76 | 77 | '; 78 | 79 | $this->assertSame($expected, $this->compiler->compileString($string)); 80 | } 81 | 82 | public function testRawBlocksDontGetMixedUpWhenSomeAreRemovedByBladeComments() 83 | { 84 | $string = '{{-- @verbatim Block #1 @endverbatim --}} @php "Block #2" @endphp'; 85 | $expected = ' '; 86 | 87 | $this->assertSame($expected, $this->compiler->compileString($string)); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeForelseStatementsTest.php: -------------------------------------------------------------------------------- 1 | getUsers() as $user) 10 | breeze 11 | @empty 12 | empty 13 | @endforelse'; 14 | $expected = 'getUsers(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $user): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> 15 | breeze 16 | popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> 17 | empty 18 | '; 19 | $this->assertEquals($expected, $this->compiler->compileString($string)); 20 | } 21 | 22 | public function testForelseStatementsAreCompiledWithUppercaseSyntax() 23 | { 24 | $string = '@forelse ($this->getUsers() AS $user) 25 | breeze 26 | @empty 27 | empty 28 | @endforelse'; 29 | $expected = 'getUsers(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $user): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> 30 | breeze 31 | popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> 32 | empty 33 | '; 34 | $this->assertEquals($expected, $this->compiler->compileString($string)); 35 | } 36 | 37 | public function testForelseStatementsAreCompiledWithMultipleLine() 38 | { 39 | $string = '@forelse ([ 40 | foo, 41 | bar, 42 | ] as $label) 43 | breeze 44 | @empty 45 | empty 46 | @endforelse'; 47 | $expected = 'addLoop($__currentLoopData); foreach($__currentLoopData as $label): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> 51 | breeze 52 | popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> 53 | empty 54 | '; 55 | $this->assertEquals($expected, $this->compiler->compileString($string)); 56 | } 57 | 58 | public function testNestedForelseStatementsAreCompiled() 59 | { 60 | $string = '@forelse ($this->getUsers() as $user) 61 | @forelse ($user->tags as $tag) 62 | breeze 63 | @empty 64 | tag empty 65 | @endforelse 66 | @empty 67 | empty 68 | @endforelse'; 69 | $expected = 'getUsers(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $user): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> 70 | tags; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $tag): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_2 = false; ?> 71 | breeze 72 | popLoop(); $loop = $__env->getLastLoop(); if ($__empty_2): ?> 73 | tag empty 74 | 75 | popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> 76 | empty 77 | '; 78 | $this->assertEquals($expected, $this->compiler->compileString($string)); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Compilers/BladeCompiler.php: -------------------------------------------------------------------------------- 1 | rawTags = [preg_quote($openTag), preg_quote($closeTag)]; 58 | } 59 | 60 | /** 61 | * Sets the content tags used for the compiler. 62 | * 63 | * @deprecated This feature was removed from Laravel (https://github.com/laravel/framework/issues/17736) 64 | * 65 | * @param string $openTag 66 | * @param string $closeTag 67 | * @param bool $escaped 68 | * @return void 69 | */ 70 | public function setContentTags($openTag, $closeTag, $escaped = true) 71 | { 72 | $this->contentTags = [preg_quote($openTag), preg_quote($closeTag)]; 73 | } 74 | 75 | /** 76 | * Sets the escape tags used for the compiler. 77 | * 78 | * @deprecated This feature was removed from Laravel (https://github.com/laravel/framework/issues/17736) 79 | * 80 | * @param string $openTag 81 | * @param string $closeTag 82 | * @param bool $escaped 83 | * @return void 84 | */ 85 | public function setEscapeTags($openTag, $closeTag, $escaped = true) 86 | { 87 | $this->escapedTags = [preg_quote($openTag), preg_quote($closeTag)]; 88 | } 89 | 90 | /** 91 | * Enable/Disable force recompile of templates. 92 | * 93 | * @param bool $recompile 94 | * @return void 95 | */ 96 | public function setForceTemplateRecompile($recompile = true) { 97 | $this->forceTemplateRecompile = $recompile; 98 | } 99 | 100 | /** 101 | * Determine if the view at the given path is expired. 102 | * 103 | * @param string $path 104 | * @return bool 105 | */ 106 | public function isExpired($path) 107 | { 108 | 109 | // adds ability to force template recompile 110 | if ($this->forceTemplateRecompile) { 111 | return true; 112 | } 113 | 114 | return parent::isExpired($path); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeEchoTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('', $this->compiler->compileString('{!!$name!!}')); 10 | $this->assertEquals('', $this->compiler->compileString('{!! $name !!}')); 11 | $this->assertEquals('', $this->compiler->compileString('{!! 12 | $name 13 | !!}')); 14 | 15 | $this->assertEquals('', $this->compiler->compileString('{{{$name}}}')); 16 | $this->assertEquals('', $this->compiler->compileString('{{$name}}')); 17 | $this->assertEquals('', $this->compiler->compileString('{{ $name }}')); 18 | $this->assertEquals('', $this->compiler->compileString('{{ 19 | $name 20 | }}')); 21 | $this->assertEquals("\n\n", $this->compiler->compileString("{{ \$name }}\n")); 22 | $this->assertEquals("\r\n\r\n", $this->compiler->compileString("{{ \$name }}\r\n")); 23 | $this->assertEquals("\n\n", $this->compiler->compileString("{{ \$name }}\n")); 24 | $this->assertEquals("\r\n\r\n", $this->compiler->compileString("{{ \$name }}\r\n")); 25 | 26 | $this->assertEquals('', 27 | $this->compiler->compileString('{{ "Hello world or foo" }}')); 28 | $this->assertEquals('', 29 | $this->compiler->compileString('{{"Hello world or foo"}}')); 30 | $this->assertEquals('', $this->compiler->compileString('{{$foo + $or + $baz}}')); 31 | $this->assertEquals('', $this->compiler->compileString('{{ 32 | "Hello world or foo" 33 | }}')); 34 | 35 | $this->assertEquals('', 36 | $this->compiler->compileString('{{ \'Hello world or foo\' }}')); 37 | $this->assertEquals('', 38 | $this->compiler->compileString('{{\'Hello world or foo\'}}')); 39 | $this->assertEquals('', $this->compiler->compileString('{{ 40 | \'Hello world or foo\' 41 | }}')); 42 | 43 | $this->assertEquals('', 44 | $this->compiler->compileString('{{ myfunc(\'foo or bar\') }}')); 45 | $this->assertEquals('', 46 | $this->compiler->compileString('{{ myfunc("foo or bar") }}')); 47 | $this->assertEquals('', 48 | $this->compiler->compileString('{{ myfunc("$name or \'foo\'") }}')); 49 | } 50 | 51 | public function testEscapedWithAtEchosAreCompiled() 52 | { 53 | $this->assertEquals('{{$name}}', $this->compiler->compileString('@{{$name}}')); 54 | $this->assertEquals('{{ $name }}', $this->compiler->compileString('@{{ $name }}')); 55 | $this->assertEquals('{{ 56 | $name 57 | }}', 58 | $this->compiler->compileString('@{{ 59 | $name 60 | }}')); 61 | $this->assertEquals('{{ $name }} 62 | ', 63 | $this->compiler->compileString('@{{ $name }} 64 | ')); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/StringView.php: -------------------------------------------------------------------------------- 1 | view = (is_array($view))?(object) $view:$view; 33 | $this->path = $path; 34 | $this->engine = $engine; 35 | $this->factory = $factory; 36 | 37 | $this->data = $data instanceof Arrayable ? $data->toArray() : (array) $data; 38 | 39 | // check if view has secondsTemplateCacheExpires set, or get from config 40 | if ( !property_exists($this->view, "secondsTemplateCacheExpires") || !is_numeric($this->view->secondsTemplateCacheExpires) ) { 41 | $this->view->secondsTemplateCacheExpires = config('blade.secondsTemplateCacheExpires'); 42 | if ( is_null($this->view->secondsTemplateCacheExpires) ) { 43 | $this->view->secondsTemplateCacheExpires = 0; 44 | } 45 | } 46 | 47 | // this is the actually blade template data 48 | if ( !property_exists($this->view, "template") ) 49 | { 50 | // is the same as sending a blank template file 51 | $this->view->template = ''; 52 | } 53 | 54 | // each template requires a unique cache key, or generate one 55 | if ( !property_exists($this->view, "cache_key") ) 56 | { 57 | // special, to catch if template is empty 58 | if (empty($this->view->template)) { 59 | $this->view->cache_key = md5('_empty_template_'); 60 | } else { 61 | $this->view->cache_key = md5($this->view->template); 62 | } 63 | } 64 | } 65 | 66 | public function getViewTemplate() { 67 | return $this->view->template; 68 | } 69 | 70 | /** 71 | * Get the name of the view. 72 | * 73 | * @return string 74 | */ 75 | public function getName() 76 | { 77 | return (isset($this->view->template)?md5($this->view->template):'StringViewTemplate'); 78 | } 79 | 80 | /** 81 | * Get the evaluated contents of the view. 82 | * 83 | * @return string 84 | */ 85 | protected function getContents() 86 | { 87 | 88 | 89 | /** 90 | * This property will be added to models being compiled with StringView 91 | * to keep track of which field in the model is being compiled 92 | */ 93 | //$this->path->__string_blade_compiler_template_field = $this->template_field; 94 | 95 | if (is_null($this->path)) { 96 | return $this->engine->get($this->view, $this->gatherData()); 97 | } 98 | 99 | return $this->engine->get($this->path, $this->gatherData()); 100 | //return parent::getContents(); 101 | } 102 | 103 | /** 104 | * Checks if a string is a valid timestamp. 105 | * from https://gist.github.com/sepehr/6351385 106 | * 107 | * @param string $timestamp Timestamp to validate. 108 | * 109 | * @return bool 110 | */ 111 | function is_timestamp($timestamp) 112 | { 113 | $check = (is_int($timestamp) OR is_float($timestamp)) 114 | ? $timestamp 115 | : (string) (int) $timestamp; 116 | 117 | return ($check === $timestamp) 118 | AND ( (int) $timestamp <= PHP_INT_MAX) 119 | AND ( (int) $timestamp >= ~PHP_INT_MAX); 120 | } 121 | 122 | 123 | } -------------------------------------------------------------------------------- /tests/View/Blade/BladeForeachStatementsTest.php: -------------------------------------------------------------------------------- 1 | getUsers() as $user) 10 | test 11 | @endforeach'; 12 | $expected = 'getUsers(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $user): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> 13 | test 14 | popLoop(); $loop = $__env->getLastLoop(); ?>'; 15 | $this->assertEquals($expected, $this->compiler->compileString($string)); 16 | } 17 | 18 | public function testForeachStatementsAreCompileWithUppercaseSyntax() 19 | { 20 | $string = '@foreach ($this->getUsers() AS $user) 21 | test 22 | @endforeach'; 23 | $expected = 'getUsers(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $user): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> 24 | test 25 | popLoop(); $loop = $__env->getLastLoop(); ?>'; 26 | $this->assertEquals($expected, $this->compiler->compileString($string)); 27 | } 28 | 29 | public function testForeachStatementsAreCompileWithMultipleLine() 30 | { 31 | $string = '@foreach ([ 32 | foo, 33 | bar, 34 | ] as $label) 35 | test 36 | @endforeach'; 37 | $expected = 'addLoop($__currentLoopData); foreach($__currentLoopData as $label): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> 41 | test 42 | popLoop(); $loop = $__env->getLastLoop(); ?>'; 43 | $this->assertEquals($expected, $this->compiler->compileString($string)); 44 | } 45 | 46 | public function testNestedForeachStatementsAreCompiled() 47 | { 48 | $string = '@foreach ($this->getUsers() as $user) 49 | user info 50 | @foreach ($user->tags as $tag) 51 | tag info 52 | @endforeach 53 | @endforeach'; 54 | $expected = 'getUsers(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $user): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> 55 | user info 56 | tags; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $tag): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> 57 | tag info 58 | popLoop(); $loop = $__env->getLastLoop(); ?> 59 | popLoop(); $loop = $__env->getLastLoop(); ?>'; 60 | $this->assertEquals($expected, $this->compiler->compileString($string)); 61 | } 62 | 63 | public function testLoopContentHolderIsExtractedFromForeachStatements() 64 | { 65 | $string = '@foreach ($some_uSers1 as $user)'; 66 | $expected = 'addLoop($__currentLoopData); foreach($__currentLoopData as $user): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>'; 67 | $this->assertEquals($expected, $this->compiler->compileString($string)); 68 | 69 | $string = '@foreach ($users->get() as $user)'; 70 | $expected = 'get(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $user): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>'; 71 | $this->assertEquals($expected, $this->compiler->compileString($string)); 72 | 73 | $string = '@foreach (range(1, 4) as $user)'; 74 | $expected = 'addLoop($__currentLoopData); foreach($__currentLoopData as $user): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>'; 75 | $this->assertEquals($expected, $this->compiler->compileString($string)); 76 | 77 | $string = '@foreach ( $users as $user)'; 78 | $expected = 'addLoop($__currentLoopData); foreach($__currentLoopData as $user): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>'; 79 | $this->assertEquals($expected, $this->compiler->compileString($string)); 80 | 81 | $string = '@foreach ($tasks as $task)'; 82 | $expected = 'addLoop($__currentLoopData); foreach($__currentLoopData as $task): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>'; 83 | $this->assertEquals($expected, $this->compiler->compileString($string)); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Compilers/StringBladeCompiler.php: -------------------------------------------------------------------------------- 1 | files = $files; 28 | $this->cachePath = $cachePath; 29 | } 30 | 31 | public function setViewData($viewData) { 32 | $this->viewData = $viewData; 33 | } 34 | 35 | /** 36 | * Compile the view at the given path. 37 | * 38 | * @param object $viewData 39 | * @return void 40 | */ 41 | public function compile($viewData = null) 42 | { 43 | if (!is_null($viewData)) { 44 | $this->viewData = $viewData; 45 | } 46 | 47 | if (property_exists($this->viewData, 'cache_key')) 48 | { 49 | $this->setPath($this->viewData->cache_key); 50 | } 51 | 52 | $contents = $this->compileString( 53 | $this->viewData->template 54 | ); 55 | 56 | $tokens = $this->getOpenAndClosingPhpTokens($contents); 57 | 58 | // If the tokens we retrieved from the compiled contents have at least 59 | // one opening tag and if that last token isn't the closing tag, we 60 | // need to close the statement before adding the path at the end. 61 | if ($tokens->isNotEmpty() && $tokens->last() !== T_CLOSE_TAG) { 62 | $contents .= ' ?>'; 63 | } 64 | 65 | if (isset($this->viewData->templateRefKey)) { 66 | $contents .= "viewData->templateRefKey} ENDPATH**/ ?>"; 67 | } 68 | 69 | if (! is_null($this->cachePath)) { 70 | $this->files->put( 71 | $this->getCompiledPath($this->viewData), $contents 72 | ); 73 | } 74 | } 75 | 76 | /** 77 | * Get the path to the compiled version of a view. 78 | * 79 | * @param string $path 80 | * @return string 81 | */ 82 | public function getCompiledPath($viewData) 83 | { 84 | if (!property_exists($viewData, 'cache_key')) 85 | { 86 | $cacheKey = Str::random(40); 87 | while (in_array($cacheKey, $this->use_cache_keys)) { 88 | $cacheKey = Str::random(40); 89 | } 90 | 91 | $viewData->cache_key = $cacheKey; 92 | } 93 | 94 | return $this->cachePath.'/'.sha1($viewData->cache_key).'.php'; 95 | } 96 | 97 | /** 98 | * Determine if the view at the given path is expired. 99 | * 100 | * @param object $viewData 101 | * @return bool 102 | */ 103 | public function isExpired($viewData) 104 | { 105 | 106 | // adds ability to force template recompile 107 | if ($this->forceTemplateRecompile) { 108 | return true; 109 | } 110 | 111 | $compiled = $this->getCompiledPath($viewData); 112 | 113 | // If the compiled file doesn't exist we will indicate that the view is expired 114 | // so that it can be re-compiled. Else, we will verify the last modification 115 | // of the views is less than the modification times of the compiled views. 116 | if ( ! $this->cachePath || ! $this->files->exists($compiled)) 117 | { 118 | return true; 119 | } 120 | 121 | // If set to 0, then return cache has expired 122 | if (property_exists($viewData, 'secondsTemplateCacheExpires')) { 123 | if ($viewData->secondsTemplateCacheExpires == 0) { 124 | return true; 125 | } 126 | } else { 127 | $viewData->secondsTemplateCacheExpires = 0; 128 | } 129 | 130 | // Note: The lastModified time for a file on homestead will use the time from the host system. 131 | // This means the vm time could be off, so setting the timeout to seconds may not work as expected. 132 | 133 | return time() >= ($this->files->lastModified($compiled) + $viewData->secondsTemplateCacheExpires) ; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Factory.php: -------------------------------------------------------------------------------- 1 | 'blade', 55 | 'php' => 'php', 56 | 'css' => 'file', 57 | ]; 58 | 59 | /** 60 | * The view composer events. 61 | * 62 | * @var array 63 | */ 64 | protected $composers = []; 65 | 66 | /** 67 | * The number of active rendering operations. 68 | * 69 | * @var int 70 | */ 71 | protected $renderCount = 0; 72 | 73 | /** 74 | * Get the evaluated view contents for the given view. 75 | * 76 | * @param string|array $view 77 | * @param array $data 78 | * @param array $mergeData 79 | * @return \Illuminate\Contracts\View\View|\Wpb\String_Blade_Compiler\StringView 80 | */ 81 | public function make($view, $data = [], $mergeData = []) 82 | { 83 | $data = array_merge($mergeData, $this->parseData($data)); 84 | 85 | // For string rendering 86 | if (is_array($view)) { 87 | return tap($this->stringViewInstance($view, $data), function ($view) { 88 | $this->callCreator($view); 89 | }); 90 | } 91 | 92 | $path = $this->finder->find( 93 | $view = $this->normalizeName($view) 94 | ); 95 | 96 | // Next, we will create the view instance and call the view creator for the view 97 | // which can set any data, etc. Then we will return the view instance back to 98 | // the caller for rendering or performing other view manipulations on this. 99 | 100 | return tap($this->viewInstance($view, $path, $data), function ($view) { 101 | $this->callCreator($view); 102 | }); 103 | } 104 | 105 | /** 106 | * Create a new string view instance from the given arguments. 107 | * 108 | * @param string|array $view 109 | * @param array $data 110 | * @return StringView 111 | */ 112 | protected function stringViewInstance($view, $data) 113 | { 114 | return new StringView($this, $this->engines->resolve('stringblade'), $view, null, $data); 115 | } 116 | 117 | /** 118 | * Flush all of the section contents if done rendering. 119 | * 120 | * @return void 121 | */ 122 | public function flushStateIfDoneRendering() 123 | { 124 | if ($this->doneRendering()) { 125 | $this->flushState(); 126 | } 127 | } 128 | 129 | /** 130 | * Flush all of the factory state like sections and stacks. 131 | * 132 | * @return void 133 | */ 134 | public function flushState() 135 | { 136 | $this->renderCount = 0; 137 | 138 | $this->flushSections(); 139 | $this->flushStacks(); 140 | } 141 | 142 | /** 143 | * Get the appropriate view engine for the given string key. 144 | * 145 | * @param string $stringkey 146 | * @return \Illuminate\Contracts\View\Engine 147 | * 148 | * ['file', 'php', 'blade', 'stringblade'] in StringBladeServiceProvider:registerEngineResolver 149 | * 150 | * @throws \InvalidArgumentException 151 | */ 152 | public function getEngineFromStringKey($stringkey) 153 | { 154 | // resolve function throws error if $stringkey is not a registered engine 155 | return $this->engines->resolve($stringkey); 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /src/StringBladeServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom( 23 | __DIR__ . '/../config/blade.php', 'blade' 24 | ); 25 | 26 | $this->registerFactory(); 27 | $this->registerViewFinder(); 28 | $this->registerBladeCompiler(); 29 | $this->registerStringbladeCompiler(); 30 | $this->registerEngineResolver(); 31 | 32 | } 33 | 34 | /** 35 | * Register the view environment. 36 | * 37 | * @return void 38 | */ 39 | public function registerFactory() 40 | { 41 | $this->app->singleton('view', function ($app) { 42 | // Next we need to grab the engine resolver instance that will be used by the 43 | // environment. The resolver will be used by an environment to get each of 44 | // the various engine implementations such as plain PHP or Blade engine. 45 | $resolver = $app['view.engine.resolver']; 46 | 47 | $finder = $app['view.finder']; 48 | 49 | $factory = $this->createFactory($resolver, $finder, $app['events']); 50 | 51 | // We will also set the container instance on this view environment since the 52 | // view composers may be classes registered in the container, which allows 53 | // for great testable, flexible composers for the application developer. 54 | $factory->setContainer($app); 55 | 56 | $factory->share('app', $app); 57 | 58 | return $factory; 59 | }); 60 | } 61 | 62 | /** 63 | * Create a new Factory Instance. 64 | * 65 | * @param \Illuminate\View\Engines\EngineResolver $resolver 66 | * @param \Illuminate\View\ViewFinderInterface $finder 67 | * @param \Illuminate\Contracts\Events\Dispatcher $events 68 | * @return \Illuminate\View\Factory 69 | */ 70 | protected function createFactory($resolver, $finder, $events) 71 | { 72 | return new Factory($resolver, $finder, $events); 73 | } 74 | 75 | /** 76 | * Register the view finder implementation. 77 | * 78 | * @return void 79 | */ 80 | public function registerViewFinder() 81 | { 82 | // since view.find should be registered, lets get the paths and hints - in case they have changed 83 | $oldFinder = []; 84 | if ($this->app->resolved('view.finder')) { 85 | $oldFinder['paths'] = $this->app['view']->getFinder()->getPaths(); 86 | $oldFinder['hints'] = $this->app['view']->getFinder()->getHints(); 87 | } 88 | 89 | // recreate the view.finder 90 | $this->app->bind('view.finder', function ($app) use ($oldFinder) { 91 | 92 | $paths = (isset($oldFinder['paths'])) ? array_unique(array_merge($app['config']['view.paths'], $oldFinder['paths']), SORT_REGULAR) : $app['config']['view.paths']; 93 | 94 | $viewFinder = new FileViewFinder($app['files'], $paths); 95 | 96 | if (!empty($oldFinder['hints'])) { 97 | array_walk($oldFinder['hints'], function ($value, $key) use ($viewFinder) { 98 | $viewFinder->addNamespace($key, $value); 99 | }); 100 | } 101 | 102 | return $viewFinder; 103 | }); 104 | } 105 | 106 | /** 107 | * Register the engine resolver instance. 108 | * 109 | * @return void 110 | */ 111 | public function registerEngineResolver() 112 | { 113 | // recreate the resolver, adding stringblade 114 | $this->app->singleton('view.engine.resolver', function ($app) { 115 | $resolver = new EngineResolver; 116 | 117 | // Next, we will register the various view engines with the resolver so that the 118 | // environment will resolve the engines needed for various views based on the 119 | // extension of view file. We call a method for each of the view's engines. 120 | foreach (['file', 'php', 'blade', 'stringblade'] as $engine) { 121 | $this->{'register' . ucfirst($engine) . 'Engine'}($resolver); 122 | } 123 | 124 | return $resolver; 125 | }); 126 | } 127 | 128 | /** 129 | * Register the String Blade compiler implementation. 130 | * 131 | * @return void 132 | */ 133 | public function registerStringbladeCompiler() 134 | { 135 | // The Compiler engine requires an instance of the CompilerInterface, which in 136 | // this case will be the Blade compiler, so we'll first create the compiler 137 | // instance to pass into the engine so it can compile the views properly. 138 | $this->app->singleton('stringblade.compiler', function () { 139 | $cache = $this->app['config']['view.compiled']; 140 | return new StringBladeCompiler($this->app['files'], $cache); 141 | }); 142 | } 143 | 144 | /** 145 | * Register the StringBlade engine implementation. 146 | * 147 | * @param \Illuminate\View\Engines\EngineResolver $resolver 148 | * @return void 149 | */ 150 | public function registerStringbladeEngine($resolver) 151 | { 152 | $resolver->register('stringblade', function () { 153 | return new CompilerEngine($this->app['stringblade.compiler']); 154 | }); 155 | } 156 | 157 | /** 158 | * Bootstrap any application services. 159 | * 160 | * @return void 161 | */ 162 | public function boot() 163 | { 164 | if (config('blade.autoload_custom_directives')) { 165 | $blade = app('blade.compiler'); 166 | $string_blade = app('stringblade.compiler'); 167 | 168 | collect($blade->getCustomDirectives()) 169 | ->each(function ($directive, $name) use ($string_blade) { 170 | $string_blade->directive($name, $directive); 171 | }); 172 | } 173 | } 174 | 175 | /** 176 | * Get the services provided by the provider. 177 | * 178 | * @return array 179 | */ 180 | public function provides() 181 | { 182 | return [ 183 | StringBlade::class, 184 | 'view', 185 | ViewFactory::class, 186 | ]; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /tests/View/Blade/BladeCustomTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(' ', $this->compiler->compileString("@if(\$test) @endif")); 10 | } 11 | 12 | public function testMixingYieldAndEcho() 13 | { 14 | $this->assertEquals('yieldContent(\'title\'); ?> - ', $this->compiler->compileString("@yield('title') - {{Config::get('site.title')}}")); 15 | } 16 | 17 | public function testCustomExtensionsAreCompiled() 18 | { 19 | $this->compiler->extend(function ($value) { 20 | return str_replace('foo', 'bar', $value); 21 | }); 22 | $this->assertEquals('bar', $this->compiler->compileString('foo')); 23 | } 24 | 25 | public function testCustomStatements() 26 | { 27 | $this->assertCount(0, $this->compiler->getCustomDirectives()); 28 | $this->compiler->directive('customControl', function ($expression) { 29 | return ""; 30 | }); 31 | $this->assertCount(1, $this->compiler->getCustomDirectives()); 32 | 33 | $string = '@if($foo) 34 | @customControl(10, $foo, \'bar\') 35 | @endif'; 36 | $expected = ' 37 | 38 | '; 39 | $this->assertEquals($expected, $this->compiler->compileString($string)); 40 | } 41 | 42 | public function testCustomShortStatements() 43 | { 44 | $this->compiler->directive('customControl', function ($expression) { 45 | return ''; 46 | }); 47 | 48 | $string = '@customControl'; 49 | $expected = ''; 50 | $this->assertEquals($expected, $this->compiler->compileString($string)); 51 | } 52 | 53 | public function testValidCustomNames() 54 | { 55 | $this->assertNull($this->compiler->directive('custom', function () { 56 | })); 57 | $this->assertNull($this->compiler->directive('custom_custom', function () { 58 | })); 59 | $this->assertNull($this->compiler->directive('customCustom', function () { 60 | })); 61 | $this->assertNull($this->compiler->directive('custom::custom', function () { 62 | })); 63 | } 64 | 65 | public function testInvalidCustomNames() 66 | { 67 | $this->expectException(\InvalidArgumentException::class); 68 | $this->expectExceptionMessage('The directive name [custom-custom] is not valid.'); 69 | $this->compiler->directive('custom-custom', function () { 70 | }); 71 | } 72 | 73 | public function testInvalidCustomNames2() 74 | { 75 | $this->expectException(\InvalidArgumentException::class); 76 | $this->expectExceptionMessage('The directive name [custom:custom] is not valid.'); 77 | $this->compiler->directive('custom:custom', function () { 78 | }); 79 | } 80 | 81 | public function testCustomExtensionOverwritesCore() 82 | { 83 | $this->compiler->directive('foreach', function ($expression) { 84 | return ''; 85 | }); 86 | 87 | $string = '@foreach'; 88 | $expected = ''; 89 | $this->assertEquals($expected, $this->compiler->compileString($string)); 90 | } 91 | 92 | public function testCustomConditions() 93 | { 94 | $this->compiler->if('custom', function ($user) { 95 | return true; 96 | }); 97 | 98 | $string = '@custom($user) 99 | @endcustom'; 100 | $expected = ' 101 | '; 102 | $this->assertEquals($expected, $this->compiler->compileString($string)); 103 | } 104 | 105 | public function testCustomIfElseConditions() 106 | { 107 | $this->compiler->if('custom', function ($anything) { 108 | return true; 109 | }); 110 | 111 | $string = '@custom($user) 112 | @elsecustom($product) 113 | @else 114 | @endcustom'; 115 | $expected = ' 116 | 117 | 118 | '; 119 | $this->assertEquals($expected, $this->compiler->compileString($string)); 120 | } 121 | 122 | public function testCustomConditionsAccepts0AsArgument() 123 | { 124 | $this->compiler->if('custom', function ($number) { 125 | return true; 126 | }); 127 | 128 | $string = '@custom(0) 129 | @elsecustom(0) 130 | @endcustom'; 131 | $expected = ' 132 | 133 | '; 134 | $this->assertEquals($expected, $this->compiler->compileString($string)); 135 | } 136 | 137 | public function testCustomComponents() 138 | { 139 | $this->compiler->component('app.components.alert', 'alert'); 140 | 141 | $string = '@alert 142 | @endalert'; 143 | $expected = 'startComponent(\'app.components.alert\'); ?> 144 | renderComponent(); ?>'; 145 | $this->assertEquals($expected, $this->compiler->compileString($string)); 146 | } 147 | 148 | public function testCustomComponentsWithSlots() 149 | { 150 | $this->compiler->component('app.components.alert', 'alert'); 151 | 152 | $string = '@alert([\'type\' => \'danger\']) 153 | @endalert'; 154 | $expected = 'startComponent(\'app.components.alert\', [\'type\' => \'danger\']); ?> 155 | renderComponent(); ?>'; 156 | $this->assertEquals($expected, $this->compiler->compileString($string)); 157 | } 158 | 159 | public function testCustomComponentsDefaultAlias() 160 | { 161 | $this->compiler->component('app.components.alert'); 162 | 163 | $string = '@alert 164 | @endalert'; 165 | $expected = 'startComponent(\'app.components.alert\'); ?> 166 | renderComponent(); ?>'; 167 | $this->assertEquals($expected, $this->compiler->compileString($string)); 168 | } 169 | 170 | public function testCustomComponentsWithExistingDirective() 171 | { 172 | $this->compiler->component('app.components.foreach'); 173 | 174 | $string = '@foreach 175 | @endforeach'; 176 | $expected = 'startComponent(\'app.components.foreach\'); ?> 177 | renderComponent(); ?>'; 178 | $this->assertEquals($expected, $this->compiler->compileString($string)); 179 | } 180 | 181 | public function testCustomIncludes() 182 | { 183 | $this->compiler->include('app.includes.input', 'input'); 184 | 185 | $string = '@input'; 186 | $expected = 'make(\'app.includes.input\', [], \Illuminate\Support\Arr::except(get_defined_vars(), [\'__data\', \'__path\']))->render(); ?>'; 187 | $this->assertEquals($expected, $this->compiler->compileString($string)); 188 | } 189 | 190 | public function testCustomIncludesWithData() 191 | { 192 | $this->compiler->include('app.includes.input', 'input'); 193 | 194 | $string = '@input([\'type\' => \'email\'])'; 195 | $expected = 'make(\'app.includes.input\', [\'type\' => \'email\'], \Illuminate\Support\Arr::except(get_defined_vars(), [\'__data\', \'__path\']))->render(); ?>'; 196 | $this->assertEquals($expected, $this->compiler->compileString($string)); 197 | } 198 | 199 | public function testCustomIncludesDefaultAlias() 200 | { 201 | $this->compiler->include('app.includes.input'); 202 | 203 | $string = '@input'; 204 | $expected = 'make(\'app.includes.input\', [], \Illuminate\Support\Arr::except(get_defined_vars(), [\'__data\', \'__path\']))->render(); ?>'; 205 | $this->assertEquals($expected, $this->compiler->compileString($string)); 206 | } 207 | 208 | public function testCustomIncludesWithExistingDirective() 209 | { 210 | $this->compiler->include('app.includes.foreach'); 211 | 212 | $string = '@foreach'; 213 | $expected = 'make(\'app.includes.foreach\', [], \Illuminate\Support\Arr::except(get_defined_vars(), [\'__data\', \'__path\']))->render(); ?>'; 214 | $this->assertEquals($expected, $this->compiler->compileString($string)); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /tests/View/ViewStringBladeCompilerTest.php: -------------------------------------------------------------------------------- 1 | getFiles(), __DIR__); 21 | $files->shouldReceive('exists')->once()->with(__DIR__.'/'.sha1('foo').'.php')->andReturn(false); 22 | $this->assertTrue($compiler->isExpired((object)['cache_key' => 'foo'])); 23 | } 24 | 25 | public function testCannotConstructWithBadCachePath() 26 | { 27 | /* StringBladeComplier can have a empty cache path */ 28 | 29 | //$this->expectException(InvalidArgumentException::class); 30 | //$this->expectExceptionMessage('Please provide a valid cache path.'); 31 | 32 | //new BladeCompiler($this->getFiles(), null); 33 | $this->assertTrue(true); 34 | } 35 | 36 | public function testIsExpiredReturnsTrueWhenModificationTimesWarrant() 37 | { 38 | $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__); 39 | $files->shouldReceive('exists')->once()->with(__DIR__.'/'.sha1('foo').'.php')->andReturn(true); 40 | //$files->shouldReceive('lastModified')->once()->with('foo')->andReturn(100); 41 | $files->shouldReceive('lastModified')->once()->with(__DIR__.'/'.sha1('foo').'.php')->andReturn(0); 42 | 43 | $this->assertTrue($compiler->isExpired((object)['cache_key' => 'foo'])); 44 | } 45 | 46 | public function testCompilePathIsProperlyCreated() 47 | { 48 | $compiler = new BladeCompiler($this->getFiles(), __DIR__); 49 | $this->assertEquals(__DIR__.'/'.sha1('foo').'.php', $compiler->getCompiledPath((object)['cache_key' => 'foo'])); 50 | } 51 | 52 | public function testCompileCompilesFileAndReturnsContents() 53 | { 54 | $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__); 55 | //$files->shouldReceive('get')->once()->with('foo')->andReturn('Hello World'); 56 | $files->shouldReceive('put')->once()->with(__DIR__.'/'.sha1('foo').'.php', 'Hello World'); 57 | $compiler->compile((object)['template' => 'Hello World', 'templateRefKey' => 'foo', 'cache_key' => 'foo']); 58 | } 59 | 60 | public function testCompileCompilesAndGetThePath() 61 | { 62 | $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__); 63 | //$files->shouldReceive('get')->once()->with('foo')->andReturn('Hello World'); 64 | $files->shouldReceive('put')->once()->with(__DIR__.'/'.sha1('foo').'.php', 'Hello World'); 65 | $compiler->compile((object)['template' => 'Hello World', 'templateRefKey' => 'foo', 'cache_key' => 'foo']); 66 | $this->assertEquals('foo', $compiler->getPath()); 67 | } 68 | 69 | public function testCompileSetAndGetThePath() 70 | { 71 | $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__); 72 | $compiler->setPath('foo'); 73 | $this->assertEquals('foo', $compiler->getPath()); 74 | } 75 | 76 | public function testCompileWithPathSetBefore() 77 | { 78 | $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__); 79 | //$files->shouldReceive('get')->once()->with('foo')->andReturn('Hello World'); 80 | $files->shouldReceive('put')->once()->with(__DIR__.'/'.sha1('foo').'.php', 'Hello World'); 81 | // set path before compilation 82 | $compiler->setViewData((object)['template' => 'Hello World', 'templateRefKey' => 'foo', 'cache_key' => 'foo']); 83 | // trigger compilation with $path 84 | $compiler->compile(); 85 | $this->assertEquals('foo', $compiler->getPath()); 86 | } 87 | 88 | public function testRawTagsCanBeSetToLegacyValues() 89 | { 90 | $compiler = new BladeCompiler($this->getFiles(), __DIR__); 91 | $compiler->setEchoFormat('%s'); 92 | 93 | $this->assertEquals('', $compiler->compileString('{{{ $name }}}')); 94 | $this->assertEquals('', $compiler->compileString('{{ $name }}')); 95 | $this->assertEquals('', $compiler->compileString('{{ 96 | $name 97 | }}')); 98 | } 99 | 100 | /** 101 | * @param string $content 102 | * @param string $compiled 103 | * 104 | * @dataProvider appendViewPathDataProvider 105 | */ 106 | public function testIncludePathToTemplate($content, $compiled) 107 | { 108 | $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__); 109 | //$files->shouldReceive('get')->once()->with('foo')->andReturn($content); 110 | $files->shouldReceive('put')->once()->with(__DIR__.'/'.sha1('foo').'.php', $compiled); 111 | 112 | $compiler->compile((object)['template' => $content, 'templateRefKey' => 'foo', 'cache_key' => 'foo']); 113 | } 114 | 115 | /** 116 | * @return array 117 | */ 118 | public function appendViewPathDataProvider() 119 | { 120 | return [ 121 | 'No PHP blocks' => [ 122 | 'Hello World', 123 | 'Hello World', 124 | ], 125 | 'Single PHP block without closing ?>' => [ 126 | '', 128 | ], 129 | 'Ending PHP block.' => [ 130 | 'Hello world', 131 | 'Hello world', 132 | ], 133 | 'Ending PHP block without closing ?>' => [ 134 | 'Hello world', 136 | ], 137 | 'PHP block between content.' => [ 138 | 'Hello worldHi There', 139 | 'Hello worldHi There', 140 | ], 141 | 'Multiple PHP blocks.' => [ 142 | 'Hello worldHi ThereHello Again', 143 | 'Hello worldHi ThereHello Again', 144 | ], 145 | 'Multiple PHP blocks without closing ?>' => [ 146 | 'Hello worldHi ThereHi There', 148 | ], 149 | 'Short open echo tag' => [ 150 | 'Hello world', 152 | ], 153 | 'Echo XML declaration' => [ 154 | '\';', 155 | '\'; ?>', 156 | ], 157 | ]; 158 | } 159 | 160 | public function testDontIncludeEmptyPath() 161 | { 162 | /* Stupid test, you can't get a file without a file name */ 163 | /* 164 | $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__); 165 | $files->shouldReceive('get')->once()->with('')->andReturn('Hello World'); 166 | $files->shouldReceive('put')->once()->with(__DIR__.'/'.sha1('').'.php', 'Hello World'); 167 | $compiler->setPath(''); 168 | $compiler->compile(); 169 | */ 170 | } 171 | 172 | public function testDontIncludeNullPath() 173 | { 174 | /* Stupid test, you can't get a file named null */ 175 | /* 176 | $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__); 177 | $files->shouldReceive('get')->once()->with(null)->andReturn('Hello World'); 178 | $files->shouldReceive('put')->once()->with(__DIR__.'/'.sha1(null).'.php', 'Hello World'); 179 | $compiler->setPath(null); 180 | $compiler->compile(); 181 | */ 182 | } 183 | 184 | public function testShouldStartFromStrictTypesDeclaration() 185 | { 186 | $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__); 187 | $strictTypeDecl = "assertTrue(substr($compiler->compileString(" This is a direct extension of \Illuminate\View\View and is build to replace its usage. It will replace the default View instance. 11 | 12 | Versions 13 | ======================= 14 | | String Blade | Laravel Version | 15 | | ------------- |----------------:| 16 | | 6.0 | Laravel 8 | 17 | | 5.0 | Laravel 7 | 18 | | 4.0 | Laravel 6 | 19 | | 3.8 | Laravel 5.8 | 20 | | 3.7 | Laravel 5.7 | 21 | | 3.6 | Laravel 5.6 | 22 | | 3.5 | Laravel 5.5 | 23 | | 3.4 | Laravel 5.4 | 24 | | 3.3 | Laravel 5.2 | 25 | | 3.2 | Laravel 5.1 | 26 | | 2.* | Laravel 5 | 27 | | 1.* | Laravel 4.2 | 28 | 29 | Version 3.8 : Updates 30 | ======================= 31 | > The package has been completely rewritten, all code updated to be more in line with the Laravel version code. Several of the functions in the extended class were not needed and have been removed and the code has been cleaned up a bit. 32 | 33 | > Also updated the tests to what is available in Laravel View Tests. Some are not applicable to StringBlade. 34 | 35 | Changes, 36 | - Now uses Laravel auto registration feature. 37 | - No long need to remove ```Illuminate\View\ViewServiceProvider::class``` from ```config/app.php``` 38 | - The ability to ```setRawTags```, ```setContentTags```, and ```setEscapedTags``` have been removed from Laravel (https://github.com/laravel/framework/issues/17736). They are depreciated here. 39 | - The compiler function ```setDeleteViewCacheAfterRender```, has been deprecated as I didn't find any code where it was actually being used. 40 | - Added more tests 41 | 42 | Installation 43 | ======================= 44 | 45 | Add the package to composer.json: 46 | 47 | "require": { 48 | ... 49 | "wpb/string-blade-compiler": "VERSION" 50 | }, 51 | 52 | > To get versions 'composer show wpb/string-blade-compiler', such as 'dev-master, * 3.2.x-dev, 3.2.0, 3.0.x-dev, 3.0.0, 2.1.0, 2.0.x-dev, 2.0.0, 1.0.x-dev, 1.0.0' 53 | 54 | On packagist.org at https://packagist.org/packages/wpb/string-blade-compiler 55 | 56 | composer require "wpb/string-blade-compiler" 57 | 58 | Configuration 59 | ======================= 60 | 61 | In config\app.php, providers section: 62 | 63 | > Both the ServiceProvider and Facade Alias are auto registered by Laravel. There is no need to add them to the /config/app.php file. 64 | 65 | ~~Replace 'Illuminate\View\ViewServiceProvider::class' with 'Wpb\String_Blade_Compiler\ViewServiceProvider::class',~~ 66 | > This version does not require you to remove the registration of the original view component. Upon ServiceProvider registration it will replace the view binds with its self. 67 | 68 | ### Laravel's Autoloader 69 | 70 | > There currently is a issue in Laravel's preload process of ServiceProviders. Service providers that are registered with the autoloaded are instantiated before service providers that are set in /config/app.php. This may cause problems in prior versions of StringBladeCompiler. The version has been rewitten to account for the autoloading process. 71 | 72 | > A pull request that would load vendor service providers registerd in the ```config/app.php``` file before autoloads, was sent to Laravel/Framework and was rejected. 73 | 74 | * If you have a need to have this, or any other, package load before the vendor autoloads, do this - https://gist.github.com/TerrePorter/4d2ed616c6ebeb371775347f7378c035 75 | 76 | Config 77 | ======================= 78 | 79 | Default cache time for the compiled string template is 300 seconds (5 mins), this can be changed in the config file, env file, or when calling a view. The change is global to all string templates. 80 | 81 | `STRING_BLADE_CACHE_TIMEOUT=300` 82 | 83 | Autoloading of blade custom directives can be changed in the config and env files. 84 | 85 | `STRING_BLADE_AUTOLOAD=false` 86 | 87 | Note: If using homestead or some other vm, the host handles the filemtime of the cache file. This means the vm may have a different time than the file. If the cache is not expiring as expected, check the times between the systems. 88 | 89 | Usage 90 | ======================= 91 | 92 | This package offers a StringView facade with the same syntax as View but accepts a Array or Array Object instance instead of path to view. 93 | 94 | ### New Config Option: 95 | 96 | Laravel 5.8 BladeCompiler adds a php comment to the compiled template file ```php $contents .= "getPath()} ENDPATH**/ ?>"; ```. Since StringBladeCompiler does not have a "path" aka "template file location" that would be helpful to the developer. I have included a new config value, ```templateRefKey```. This allows the developer to tag the StringBladeCompiler for where it is used. This is for if you end up digging in to the compiled view files, it would allow you to see a tag for StingBladeCompiler files. 97 | 98 | ### Config Options: 99 | 100 | ```php 101 | // existing file template load (the original View() method 102 | return view ('bladetemplatefile',['token' => 'I am the token value']); 103 | ``` 104 | ```php 105 | // string blade template load 106 | return view (['template' => '{{$token}}'], ['token' => 'I am the token value']); 107 | ``` 108 | 109 | ```php 110 | // you can mix the view types 111 | $preset = view (['template' => '{{$token}}'], ['token' => 'I am the token value']); 112 | 113 | return view ('bladetemplatefile', ['token' => $preset]); 114 | ``` 115 | 116 | ```php 117 | // full list of options 118 | return view( 119 | array( 120 | // this actual blade template 121 | 'template' => '{{ $token1 }}', 122 | 123 | // this is the cache file key, converted to md5 124 | 'cache_key' => 'my_unique_cache_key', 125 | 126 | // number of seconds needed in order to recompile, 0 is always recompile 127 | 'secondsTemplateCacheExpires' => 1391973007, 128 | 129 | // sets the PATH comment value in the compiled file 130 | 'templateRefKey' => 'IndexController: Build function' 131 | ), 132 | array( 133 | 'token1'=> 'token 1 value' 134 | ) 135 | ); 136 | ``` 137 | 138 | > Since StringBlade is a extend class from the original View. You should be able to do anything you would normally do with a View using StringBlade. 139 | 140 | ### Blade::extend, for example : 141 | 142 | As the compilers are set up as separate instances, if you need the extend on both the string and file template you will need to attach the extend (or directive) to both compilers. 143 | 144 | ```php 145 | // allows for @continue and @break in foreach in blade templates 146 | StringBlade::extend(function($value) 147 | { 148 | return preg_replace('/(\s*)@(break|continue)(\s*)/', '$1$3', $value); 149 | }); 150 | 151 | Blade::extend(function($value) 152 | { 153 | return preg_replace('/(\s*)@(break|continue)(\s*)/', '$1$3', $value); 154 | }); 155 | ``` 156 | 157 | ### Other options, 158 | 159 | ```php 160 | // change the contente tags escaped or not 161 | StringBlade::setContentTagsEscaped(true); 162 | 163 | // for devel force templates to be rebuilt, ignores secondsTemplateCacheExpires 164 | StringBlade::setForceTemplateRecompile(true); 165 | ``` 166 | 167 | ```php 168 | // change the contente tags escaped or not 169 | Blade::setContentTagsEscaped(true); 170 | 171 | // for devel force templates to be rebuilt, ignores secondsTemplateCacheExpires 172 | Blade::setForceTemplateRecompile(true); 173 | ``` 174 | 175 | ```php 176 | // @deprecated This feature was removed from Laravel (https://github.com/laravel/framework/issues/17736) 177 | 178 | // change the tags 179 | StringBlade::setRawTags('[!!', '!!]',escapeFlag); // default {!! !!} 180 | StringBlade::setContentTags('[[', ']]',escapeFlag); // default {{ }} 181 | StringBlade::setEscapedTags('[[[', ']]]',escapeFlag); // default {{{ }}} 182 | 183 | __ Functions are still there, use at your own risk. __ 184 | ``` 185 | 186 | ~~Deleting generated compiled cach view files (v3+).Set the delete flag for the compiler being used, stringblade or blade~~ 187 | 188 | > I can't seem to find when the setting was actually used. If you want this, submit a bug and I will see about adding the ability. 189 | 190 | ```php 191 | // set flag to delete compiled view cache files after rendering for stringblade compiler 192 | View::getEngineFromStringKey('stringblade')->setDeleteViewCacheAfterRender(true); 193 | 194 | // set flag to delete compiled view cache files after rendering for blade compiler 195 | View::getEngineFromStringKey('blade')->setDeleteViewCacheAfterRender(true); 196 | ``` 197 | 198 | License 199 | ======================= 200 | 201 | string-blade-compiler is open-sourced software licensed under the MIT license 202 | -------------------------------------------------------------------------------- /tests/View/ViewTest.php: -------------------------------------------------------------------------------- 1 | getView(); 29 | $view->with('foo', 'bar'); 30 | $view->with(['baz' => 'boom']); 31 | $this->assertEquals(['foo' => 'bar', 'baz' => 'boom'], $view->getData()); 32 | 33 | $view = $this->getView(); 34 | $view->withFoo('bar')->withBaz('boom'); 35 | $this->assertEquals(['foo' => 'bar', 'baz' => 'boom'], $view->getData()); 36 | } 37 | 38 | public function testRenderProperlyRendersView() 39 | { 40 | $view = $this->getView(['foo' => 'bar']); 41 | $view->getFactory()->shouldReceive('incrementRender')->once()->ordered(); 42 | $view->getFactory()->shouldReceive('callComposer')->once()->ordered()->with($view); 43 | $view->getFactory()->shouldReceive('getShared')->once()->andReturn(['shared' => 'foo']); 44 | $view->getEngine()->shouldReceive('get')->once()->with('path', ['foo' => 'bar', 'shared' => 'foo'])->andReturn('contents'); 45 | $view->getFactory()->shouldReceive('decrementRender')->once()->ordered(); 46 | $view->getFactory()->shouldReceive('flushStateIfDoneRendering')->once(); 47 | 48 | $callback = function (View $rendered, $contents) use ($view) { 49 | $this->assertEquals($view, $rendered); 50 | $this->assertEquals('contents', $contents); 51 | }; 52 | 53 | $this->assertEquals('contents', $view->render($callback)); 54 | } 55 | 56 | public function testRenderHandlingCallbackReturnValues() 57 | { 58 | $view = $this->getView(); 59 | $view->getFactory()->shouldReceive('incrementRender'); 60 | $view->getFactory()->shouldReceive('callComposer'); 61 | $view->getFactory()->shouldReceive('getShared')->andReturn(['shared' => 'foo']); 62 | $view->getEngine()->shouldReceive('get')->andReturn('contents'); 63 | $view->getFactory()->shouldReceive('decrementRender'); 64 | $view->getFactory()->shouldReceive('flushStateIfDoneRendering'); 65 | 66 | $this->assertEquals('new contents', $view->render(function () { 67 | return 'new contents'; 68 | })); 69 | 70 | $this->assertEmpty($view->render(function () { 71 | return ''; 72 | })); 73 | 74 | $this->assertEquals('contents', $view->render(function () { 75 | // 76 | })); 77 | } 78 | 79 | public function testRenderSectionsReturnsEnvironmentSections() 80 | { 81 | $view = m::mock(View::class.'[render]', [ 82 | m::mock(Factory::class), 83 | m::mock(Engine::class), 84 | 'view', 85 | 'path', 86 | [], 87 | ]); 88 | 89 | $view->shouldReceive('render')->with(m::type(Closure::class))->once()->andReturn($sections = ['foo' => 'bar']); 90 | 91 | $this->assertEquals($sections, $view->renderSections()); 92 | } 93 | 94 | public function testSectionsAreNotFlushedWhenNotDoneRendering() 95 | { 96 | $view = $this->getView(['foo' => 'bar']); 97 | $view->getFactory()->shouldReceive('incrementRender')->twice(); 98 | $view->getFactory()->shouldReceive('callComposer')->twice()->with($view); 99 | $view->getFactory()->shouldReceive('getShared')->twice()->andReturn(['shared' => 'foo']); 100 | $view->getEngine()->shouldReceive('get')->twice()->with('path', ['foo' => 'bar', 'shared' => 'foo'])->andReturn('contents'); 101 | $view->getFactory()->shouldReceive('decrementRender')->twice(); 102 | $view->getFactory()->shouldReceive('flushStateIfDoneRendering')->twice(); 103 | 104 | $this->assertEquals('contents', $view->render()); 105 | $this->assertEquals('contents', (string) $view); 106 | } 107 | 108 | public function testViewNestBindsASubView() 109 | { 110 | $view = $this->getView(); 111 | $view->getFactory()->shouldReceive('make')->once()->with('foo', ['data']); 112 | $result = $view->nest('key', 'foo', ['data']); 113 | 114 | $this->assertInstanceOf(View::class, $result); 115 | } 116 | 117 | public function testViewAcceptsArrayableImplementations() 118 | { 119 | $arrayable = m::mock(Arrayable::class); 120 | $arrayable->shouldReceive('toArray')->once()->andReturn(['foo' => 'bar', 'baz' => ['qux', 'corge']]); 121 | 122 | $view = $this->getView($arrayable); 123 | 124 | $this->assertEquals('bar', $view->foo); 125 | $this->assertEquals(['qux', 'corge'], $view->baz); 126 | } 127 | 128 | public function testViewGettersSetters() 129 | { 130 | $view = $this->getView(['foo' => 'bar']); 131 | $this->assertEquals($view->name(), 'view'); 132 | $this->assertEquals($view->getPath(), 'path'); 133 | $data = $view->getData(); 134 | $this->assertEquals($data['foo'], 'bar'); 135 | $view->setPath('newPath'); 136 | $this->assertEquals($view->getPath(), 'newPath'); 137 | } 138 | 139 | public function testViewArrayAccess() 140 | { 141 | $view = $this->getView(['foo' => 'bar']); 142 | $this->assertInstanceOf(ArrayAccess::class, $view); 143 | $this->assertTrue($view->offsetExists('foo')); 144 | $this->assertEquals($view->offsetGet('foo'), 'bar'); 145 | $view->offsetSet('foo', 'baz'); 146 | $this->assertEquals($view->offsetGet('foo'), 'baz'); 147 | $view->offsetUnset('foo'); 148 | $this->assertFalse($view->offsetExists('foo')); 149 | } 150 | 151 | public function testViewConstructedWithObjectData() 152 | { 153 | $view = $this->getView(new DataObjectStub); 154 | $this->assertInstanceOf(ArrayAccess::class, $view); 155 | $this->assertTrue($view->offsetExists('foo')); 156 | $this->assertEquals($view->offsetGet('foo'), 'bar'); 157 | $view->offsetSet('foo', 'baz'); 158 | $this->assertEquals($view->offsetGet('foo'), 'baz'); 159 | $view->offsetUnset('foo'); 160 | $this->assertFalse($view->offsetExists('foo')); 161 | } 162 | 163 | public function testViewMagicMethods() 164 | { 165 | $view = $this->getView(['foo' => 'bar']); 166 | $this->assertTrue(isset($view->foo)); 167 | $this->assertEquals($view->foo, 'bar'); 168 | $view->foo = 'baz'; 169 | $this->assertEquals($view->foo, 'baz'); 170 | $this->assertEquals($view['foo'], $view->foo); 171 | unset($view->foo); 172 | $this->assertFalse(isset($view->foo)); 173 | $this->assertFalse($view->offsetExists('foo')); 174 | } 175 | 176 | public function testViewBadMethod() 177 | { 178 | $this->expectException(BadMethodCallException::class); 179 | $this->expectExceptionMessage('Method Wpb\String_Blade_Compiler\View::badMethodCall does not exist.'); 180 | 181 | $view = $this->getView(); 182 | $view->badMethodCall(); 183 | } 184 | 185 | public function testViewGatherDataWithRenderable() 186 | { 187 | $view = $this->getView(); 188 | $view->getFactory()->shouldReceive('incrementRender')->once()->ordered(); 189 | $view->getFactory()->shouldReceive('callComposer')->once()->ordered()->with($view); 190 | $view->getFactory()->shouldReceive('getShared')->once()->andReturn(['shared' => 'foo']); 191 | $view->getEngine()->shouldReceive('get')->once()->andReturn('contents'); 192 | $view->getFactory()->shouldReceive('decrementRender')->once()->ordered(); 193 | $view->getFactory()->shouldReceive('flushStateIfDoneRendering')->once(); 194 | 195 | $view->renderable = m::mock(Renderable::class); 196 | $view->renderable->shouldReceive('render')->once()->andReturn('text'); 197 | $this->assertEquals('contents', $view->render()); 198 | } 199 | 200 | public function testViewRenderSections() 201 | { 202 | $view = $this->getView(); 203 | $view->getFactory()->shouldReceive('incrementRender')->once()->ordered(); 204 | $view->getFactory()->shouldReceive('callComposer')->once()->ordered()->with($view); 205 | $view->getFactory()->shouldReceive('getShared')->once()->andReturn(['shared' => 'foo']); 206 | $view->getEngine()->shouldReceive('get')->once()->andReturn('contents'); 207 | $view->getFactory()->shouldReceive('decrementRender')->once()->ordered(); 208 | $view->getFactory()->shouldReceive('flushStateIfDoneRendering')->once(); 209 | 210 | $view->getFactory()->shouldReceive('getSections')->once()->andReturn(['foo', 'bar']); 211 | $sections = $view->renderSections(); 212 | $this->assertEquals($sections[0], 'foo'); 213 | $this->assertEquals($sections[1], 'bar'); 214 | } 215 | 216 | public function testWithErrors() 217 | { 218 | $view = $this->getView(); 219 | $errors = ['foo' => 'bar', 'qu' => 'ux']; 220 | $this->assertSame($view, $view->withErrors($errors)); 221 | $this->assertInstanceOf(MessageBag::class, $view->errors); 222 | $foo = $view->errors->get('foo'); 223 | $this->assertEquals($foo[0], 'bar'); 224 | $qu = $view->errors->get('qu'); 225 | $this->assertEquals($qu[0], 'ux'); 226 | $data = ['foo' => 'baz']; 227 | $this->assertSame($view, $view->withErrors(new MessageBag($data))); 228 | $foo = $view->errors->get('foo'); 229 | $this->assertEquals($foo[0], 'baz'); 230 | } 231 | 232 | protected function getView($data = []) 233 | { 234 | return new View( 235 | m::mock(Factory::class), 236 | m::mock(Engine::class), 237 | 'view', 238 | 'path', 239 | $data 240 | ); 241 | } 242 | } 243 | 244 | -------------------------------------------------------------------------------- /tests/View/StringViewTest.php: -------------------------------------------------------------------------------- 1 | getView(); 29 | $view->with('foo', 'bar'); 30 | $view->with(['baz' => 'boom']); 31 | $this->assertEquals(['foo' => 'bar', 'baz' => 'boom'], $view->getData()); 32 | 33 | $view = $this->getView(); 34 | $view->withFoo('bar')->withBaz('boom'); 35 | $this->assertEquals(['foo' => 'bar', 'baz' => 'boom'], $view->getData()); 36 | } 37 | 38 | public function testRenderProperlyRendersView() 39 | { 40 | $view = $this->getView(['foo' => 'bar']); 41 | $view->getFactory()->shouldReceive('incrementRender')->once()->ordered(); 42 | $view->getFactory()->shouldReceive('callComposer')->once()->ordered()->with($view); 43 | $view->getFactory()->shouldReceive('getShared')->once()->andReturn(['shared' => 'foo']); 44 | $view->getEngine()->shouldReceive('get')->once()->with('path', ['foo' => 'bar', 'shared' => 'foo'])->andReturn('contents'); 45 | $view->getFactory()->shouldReceive('decrementRender')->once()->ordered(); 46 | $view->getFactory()->shouldReceive('flushStateIfDoneRendering')->once(); 47 | 48 | $callback = function (View $rendered, $contents) use ($view) { 49 | $this->assertEquals($view, $rendered); 50 | $this->assertEquals('contents', $contents); 51 | }; 52 | 53 | $this->assertEquals('contents', $view->render($callback)); 54 | } 55 | 56 | public function testRenderHandlingCallbackReturnValues() 57 | { 58 | $view = $this->getView(); 59 | $view->getFactory()->shouldReceive('incrementRender'); 60 | $view->getFactory()->shouldReceive('callComposer'); 61 | $view->getFactory()->shouldReceive('getShared')->andReturn(['shared' => 'foo']); 62 | $view->getEngine()->shouldReceive('get')->andReturn('contents'); 63 | $view->getFactory()->shouldReceive('decrementRender'); 64 | $view->getFactory()->shouldReceive('flushStateIfDoneRendering'); 65 | 66 | $this->assertEquals('new contents', $view->render(function () { 67 | return 'new contents'; 68 | })); 69 | 70 | $this->assertEmpty($view->render(function () { 71 | return ''; 72 | })); 73 | 74 | $this->assertEquals('contents', $view->render(function () { 75 | // 76 | })); 77 | } 78 | 79 | public function testRenderSectionsReturnsEnvironmentSections() 80 | { 81 | $view = m::mock(View::class.'[render]', [ 82 | m::mock(Factory::class), 83 | m::mock(Engine::class), 84 | [ 85 | // this actual blade template 86 | 'template' => '{{ $token1 }}', 87 | // this is the cache file key, converted to md5 88 | 'cache_key' => 'my_unique_cache_key', 89 | // number of seconds needed in order to recompile, 0 is always recompile 90 | 'secondsTemplateCacheExpires' => 1391973007 91 | ], 92 | 'path', 93 | [], 94 | ]); 95 | 96 | $view->shouldReceive('render')->with(m::type(Closure::class))->once()->andReturn($sections = ['foo' => 'bar']); 97 | 98 | $this->assertEquals($sections, $view->renderSections()); 99 | } 100 | 101 | public function testSectionsAreNotFlushedWhenNotDoneRendering() 102 | { 103 | $view = $this->getView(['foo' => 'bar']); 104 | $view->getFactory()->shouldReceive('incrementRender')->twice(); 105 | $view->getFactory()->shouldReceive('callComposer')->twice()->with($view); 106 | $view->getFactory()->shouldReceive('getShared')->twice()->andReturn(['shared' => 'foo']); 107 | $view->getEngine()->shouldReceive('get')->twice()->with('path', ['foo' => 'bar', 'shared' => 'foo'])->andReturn('contents'); 108 | $view->getFactory()->shouldReceive('decrementRender')->twice(); 109 | $view->getFactory()->shouldReceive('flushStateIfDoneRendering')->twice(); 110 | 111 | $this->assertEquals('contents', $view->render()); 112 | $this->assertEquals('contents', (string) $view); 113 | } 114 | 115 | public function testViewNestBindsASubView() 116 | { 117 | $view = $this->getView(); 118 | $view->getFactory()->shouldReceive('make')->once()->with('foo', ['data']); 119 | $result = $view->nest('key', 'foo', ['data']); 120 | 121 | $this->assertInstanceOf(View::class, $result); 122 | } 123 | 124 | public function testViewAcceptsArrayableImplementations() 125 | { 126 | $arrayable = m::mock(Arrayable::class); 127 | $arrayable->shouldReceive('toArray')->once()->andReturn(['foo' => 'bar', 'baz' => ['qux', 'corge']]); 128 | 129 | $view = $this->getView($arrayable); 130 | 131 | $this->assertEquals('bar', $view->foo); 132 | $this->assertEquals(['qux', 'corge'], $view->baz); 133 | } 134 | 135 | public function testViewGettersSetters() 136 | { 137 | $view = $this->getView(['foo' => 'bar']); 138 | $this->assertEquals($view->name(), md5($view->getViewTemplate())); 139 | $this->assertEquals($view->getPath(), 'path'); 140 | $data = $view->getData(); 141 | $this->assertEquals($data['foo'], 'bar'); 142 | $view->setPath('newPath'); 143 | $this->assertEquals($view->getPath(), 'newPath'); 144 | } 145 | 146 | public function testViewArrayAccess() 147 | { 148 | $view = $this->getView(['foo' => 'bar']); 149 | $this->assertInstanceOf(ArrayAccess::class, $view); 150 | $this->assertTrue($view->offsetExists('foo')); 151 | $this->assertEquals($view->offsetGet('foo'), 'bar'); 152 | $view->offsetSet('foo', 'baz'); 153 | $this->assertEquals($view->offsetGet('foo'), 'baz'); 154 | $view->offsetUnset('foo'); 155 | $this->assertFalse($view->offsetExists('foo')); 156 | } 157 | 158 | public function testViewConstructedWithObjectData() 159 | { 160 | $view = $this->getView(new DataObjectStub); 161 | $this->assertInstanceOf(ArrayAccess::class, $view); 162 | $this->assertTrue($view->offsetExists('foo')); 163 | $this->assertEquals($view->offsetGet('foo'), 'bar'); 164 | $view->offsetSet('foo', 'baz'); 165 | $this->assertEquals($view->offsetGet('foo'), 'baz'); 166 | $view->offsetUnset('foo'); 167 | $this->assertFalse($view->offsetExists('foo')); 168 | } 169 | 170 | public function testViewMagicMethods() 171 | { 172 | $view = $this->getView(['foo' => 'bar']); 173 | $this->assertTrue(isset($view->foo)); 174 | $this->assertEquals($view->foo, 'bar'); 175 | $view->foo = 'baz'; 176 | $this->assertEquals($view->foo, 'baz'); 177 | $this->assertEquals($view['foo'], $view->foo); 178 | unset($view->foo); 179 | $this->assertFalse(isset($view->foo)); 180 | $this->assertFalse($view->offsetExists('foo')); 181 | } 182 | 183 | public function testViewBadMethod() 184 | { 185 | $this->expectException(BadMethodCallException::class); 186 | $this->expectExceptionMessage('Method Wpb\String_Blade_Compiler\StringView::badMethodCall does not exist.'); 187 | 188 | $view = $this->getView(); 189 | $view->badMethodCall(); 190 | } 191 | 192 | public function testViewGatherDataWithRenderable() 193 | { 194 | $view = $this->getView(); 195 | $view->getFactory()->shouldReceive('incrementRender')->once()->ordered(); 196 | $view->getFactory()->shouldReceive('callComposer')->once()->ordered()->with($view); 197 | $view->getFactory()->shouldReceive('getShared')->once()->andReturn(['shared' => 'foo']); 198 | $view->getEngine()->shouldReceive('get')->once()->andReturn('contents'); 199 | $view->getFactory()->shouldReceive('decrementRender')->once()->ordered(); 200 | $view->getFactory()->shouldReceive('flushStateIfDoneRendering')->once(); 201 | 202 | $view->renderable = m::mock(Renderable::class); 203 | $view->renderable->shouldReceive('render')->once()->andReturn('text'); 204 | $this->assertEquals('contents', $view->render()); 205 | } 206 | 207 | public function testViewRenderSections() 208 | { 209 | $view = $this->getView(); 210 | $view->getFactory()->shouldReceive('incrementRender')->once()->ordered(); 211 | $view->getFactory()->shouldReceive('callComposer')->once()->ordered()->with($view); 212 | $view->getFactory()->shouldReceive('getShared')->once()->andReturn(['shared' => 'foo']); 213 | $view->getEngine()->shouldReceive('get')->once()->andReturn('contents'); 214 | $view->getFactory()->shouldReceive('decrementRender')->once()->ordered(); 215 | $view->getFactory()->shouldReceive('flushStateIfDoneRendering')->once(); 216 | 217 | $view->getFactory()->shouldReceive('getSections')->once()->andReturn(['foo', 'bar']); 218 | $sections = $view->renderSections(); 219 | $this->assertEquals($sections[0], 'foo'); 220 | $this->assertEquals($sections[1], 'bar'); 221 | } 222 | 223 | public function testWithErrors() 224 | { 225 | $view = $this->getView(); 226 | $errors = ['foo' => 'bar', 'qu' => 'ux']; 227 | $this->assertSame($view, $view->withErrors($errors)); 228 | $this->assertInstanceOf(MessageBag::class, $view->errors); 229 | $foo = $view->errors->get('foo'); 230 | $this->assertEquals($foo[0], 'bar'); 231 | $qu = $view->errors->get('qu'); 232 | $this->assertEquals($qu[0], 'ux'); 233 | $data = ['foo' => 'baz']; 234 | $this->assertSame($view, $view->withErrors(new MessageBag($data))); 235 | $foo = $view->errors->get('foo'); 236 | $this->assertEquals($foo[0], 'baz'); 237 | } 238 | 239 | protected function getView($data = []) 240 | { 241 | return new View( 242 | m::mock(Factory::class), 243 | m::mock(Engine::class), 244 | [ 245 | // this actual blade template 246 | 'template' => '{{ $token1 }}', 247 | // this is the cache file key, converted to md5 248 | 'cache_key' => 'my_unique_cache_key', 249 | // number of seconds needed in order to recompile, 0 is always recompile 250 | 'secondsTemplateCacheExpires' => 1391973007 251 | ], 252 | 'path', 253 | $data 254 | ); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /tests/View/ViewFactoryTest.php: -------------------------------------------------------------------------------- 1 | getFactory(); 38 | $factory->getFinder()->shouldReceive('find')->once()->with('view')->andReturn('path.php'); 39 | $factory->getEngineResolver()->shouldReceive('resolve')->once()->with('php')->andReturn($engine = m::mock(Engine::class)); 40 | $factory->getFinder()->shouldReceive('addExtension')->once()->with('php'); 41 | $factory->setDispatcher(new Dispatcher); 42 | $factory->creator('view', function ($view) { 43 | $_SERVER['__test.view'] = $view; 44 | }); 45 | $factory->addExtension('php', 'php'); 46 | $view = $factory->make('view', ['foo' => 'bar'], ['baz' => 'boom']); 47 | 48 | $this->assertSame($engine, $view->getEngine()); 49 | $this->assertSame($_SERVER['__test.view'], $view); 50 | 51 | unset($_SERVER['__test.view']); 52 | } 53 | 54 | public function testExistsPassesAndFailsViews() 55 | { 56 | $factory = $this->getFactory(); 57 | $factory->getFinder()->shouldReceive('find')->once()->with('foo')->andThrow(InvalidArgumentException::class); 58 | $factory->getFinder()->shouldReceive('find')->once()->with('bar')->andReturn('path.php'); 59 | 60 | $this->assertFalse($factory->exists('foo')); 61 | $this->assertTrue($factory->exists('bar')); 62 | } 63 | 64 | public function testFirstCreatesNewViewInstanceWithProperPath() 65 | { 66 | unset($_SERVER['__test.view']); 67 | 68 | $factory = $this->getFactory(); 69 | $factory->getFinder()->shouldReceive('find')->twice()->with('view')->andReturn('path.php'); 70 | $factory->getFinder()->shouldReceive('find')->once()->with('bar')->andThrow(InvalidArgumentException::class); 71 | $factory->getEngineResolver()->shouldReceive('resolve')->once()->with('php')->andReturn($engine = m::mock(Engine::class)); 72 | $factory->getFinder()->shouldReceive('addExtension')->once()->with('php'); 73 | $factory->setDispatcher(new Dispatcher); 74 | $factory->creator('view', function ($view) { 75 | $_SERVER['__test.view'] = $view; 76 | }); 77 | $factory->addExtension('php', 'php'); 78 | $view = $factory->first(['bar', 'view'], ['foo' => 'bar'], ['baz' => 'boom']); 79 | 80 | $this->assertSame($engine, $view->getEngine()); 81 | $this->assertSame($_SERVER['__test.view'], $view); 82 | 83 | unset($_SERVER['__test.view']); 84 | } 85 | 86 | public function testFirstThrowsInvalidArgumentExceptionIfNoneFound() 87 | { 88 | $this->expectException(InvalidArgumentException::class); 89 | 90 | $factory = $this->getFactory(); 91 | $factory->getFinder()->shouldReceive('find')->once()->with('view')->andThrow(InvalidArgumentException::class); 92 | $factory->getFinder()->shouldReceive('find')->once()->with('bar')->andThrow(InvalidArgumentException::class); 93 | $factory->getEngineResolver()->shouldReceive('resolve')->with('php')->andReturn($engine = m::mock(Engine::class)); 94 | $factory->getFinder()->shouldReceive('addExtension')->with('php'); 95 | $factory->addExtension('php', 'php'); 96 | $factory->first(['bar', 'view'], ['foo' => 'bar'], ['baz' => 'boom']); 97 | } 98 | 99 | public function testRenderEachCreatesViewForEachItemInArray() 100 | { 101 | $factory = m::mock(Factory::class.'[make]', $this->getFactoryArgs()); 102 | $factory->shouldReceive('make')->once()->with('foo', ['key' => 'bar', 'value' => 'baz'])->andReturn($mockView1 = m::mock(stdClass::class)); 103 | $factory->shouldReceive('make')->once()->with('foo', ['key' => 'breeze', 'value' => 'boom'])->andReturn($mockView2 = m::mock(stdClass::class)); 104 | $mockView1->shouldReceive('render')->once()->andReturn('dayle'); 105 | $mockView2->shouldReceive('render')->once()->andReturn('rees'); 106 | 107 | $result = $factory->renderEach('foo', ['bar' => 'baz', 'breeze' => 'boom'], 'value'); 108 | 109 | $this->assertEquals('daylerees', $result); 110 | } 111 | 112 | public function testEmptyViewsCanBeReturnedFromRenderEach() 113 | { 114 | $factory = m::mock(Factory::class.'[make]', $this->getFactoryArgs()); 115 | $factory->shouldReceive('make')->once()->with('foo')->andReturn($mockView = m::mock(stdClass::class)); 116 | $mockView->shouldReceive('render')->once()->andReturn('empty'); 117 | 118 | $this->assertEquals('empty', $factory->renderEach('view', [], 'iterator', 'foo')); 119 | } 120 | 121 | public function testRawStringsMayBeReturnedFromRenderEach() 122 | { 123 | $this->assertEquals('foo', $this->getFactory()->renderEach('foo', [], 'item', 'raw|foo')); 124 | } 125 | 126 | public function testEnvironmentAddsExtensionWithCustomResolver() 127 | { 128 | $factory = $this->getFactory(); 129 | 130 | $resolver = function () { 131 | // 132 | }; 133 | 134 | $factory->getFinder()->shouldReceive('addExtension')->once()->with('foo'); 135 | $factory->getEngineResolver()->shouldReceive('register')->once()->with('bar', $resolver); 136 | $factory->getFinder()->shouldReceive('find')->once()->with('view')->andReturn('path.foo'); 137 | $factory->getEngineResolver()->shouldReceive('resolve')->once()->with('bar')->andReturn($engine = m::mock(Engine::class)); 138 | $factory->getDispatcher()->shouldReceive('dispatch'); 139 | 140 | $factory->addExtension('foo', 'bar', $resolver); 141 | 142 | $view = $factory->make('view', ['data']); 143 | $this->assertSame($engine, $view->getEngine()); 144 | } 145 | 146 | public function testAddingExtensionPrependsNotAppends() 147 | { 148 | $factory = $this->getFactory(); 149 | $factory->getFinder()->shouldReceive('addExtension')->once()->with('foo'); 150 | 151 | $factory->addExtension('foo', 'bar'); 152 | 153 | $extensions = $factory->getExtensions(); 154 | $this->assertEquals('bar', reset($extensions)); 155 | $this->assertEquals('foo', key($extensions)); 156 | } 157 | 158 | public function testPrependedExtensionOverridesExistingExtensions() 159 | { 160 | $factory = $this->getFactory(); 161 | $factory->getFinder()->shouldReceive('addExtension')->once()->with('foo'); 162 | $factory->getFinder()->shouldReceive('addExtension')->once()->with('baz'); 163 | 164 | $factory->addExtension('foo', 'bar'); 165 | $factory->addExtension('baz', 'bar'); 166 | 167 | $extensions = $factory->getExtensions(); 168 | $this->assertEquals('bar', reset($extensions)); 169 | $this->assertEquals('baz', key($extensions)); 170 | } 171 | 172 | public function testComposersAreProperlyRegistered() 173 | { 174 | $factory = $this->getFactory(); 175 | $factory->getDispatcher()->shouldReceive('listen')->once()->with('composing: foo', m::type(Closure::class)); 176 | $callback = $factory->composer('foo', function () { 177 | return 'bar'; 178 | }); 179 | $callback = $callback[0]; 180 | 181 | $this->assertEquals('bar', $callback()); 182 | } 183 | 184 | public function testComposersCanBeMassRegistered() 185 | { 186 | $factory = $this->getFactory(); 187 | $factory->getDispatcher()->shouldReceive('listen')->once()->with('composing: bar', m::type(Closure::class)); 188 | $factory->getDispatcher()->shouldReceive('listen')->once()->with('composing: qux', m::type(Closure::class)); 189 | $factory->getDispatcher()->shouldReceive('listen')->once()->with('composing: foo', m::type(Closure::class)); 190 | $composers = $factory->composers([ 191 | 'foo' => 'bar', 192 | 'baz@baz' => ['qux', 'foo'], 193 | ]); 194 | 195 | $this->assertCount(3, $composers); 196 | $reflections = [ 197 | new ReflectionFunction($composers[0]), 198 | new ReflectionFunction($composers[1]), 199 | ]; 200 | $this->assertEquals(['class' => 'foo', 'method' => 'compose'], $reflections[0]->getStaticVariables()); 201 | $this->assertEquals(['class' => 'baz', 'method' => 'baz'], $reflections[1]->getStaticVariables()); 202 | } 203 | 204 | public function testClassCallbacks() 205 | { 206 | $factory = $this->getFactory(); 207 | $factory->getDispatcher()->shouldReceive('listen')->once()->with('composing: foo', m::type(Closure::class)); 208 | $factory->setContainer($container = m::mock(Container::class)); 209 | $container->shouldReceive('make')->once()->with('FooComposer')->andReturn($composer = m::mock(stdClass::class)); 210 | $composer->shouldReceive('compose')->once()->with('view')->andReturn('composed'); 211 | $callback = $factory->composer('foo', 'FooComposer'); 212 | $callback = $callback[0]; 213 | 214 | $this->assertEquals('composed', $callback('view')); 215 | } 216 | 217 | public function testClassCallbacksWithMethods() 218 | { 219 | $factory = $this->getFactory(); 220 | $factory->getDispatcher()->shouldReceive('listen')->once()->with('composing: foo', m::type(Closure::class)); 221 | $factory->setContainer($container = m::mock(Container::class)); 222 | $container->shouldReceive('make')->once()->with('FooComposer')->andReturn($composer = m::mock(stdClass::class)); 223 | $composer->shouldReceive('doComposer')->once()->with('view')->andReturn('composed'); 224 | $callback = $factory->composer('foo', 'FooComposer@doComposer'); 225 | $callback = $callback[0]; 226 | 227 | $this->assertEquals('composed', $callback('view')); 228 | } 229 | 230 | public function testCallComposerCallsProperEvent() 231 | { 232 | $factory = $this->getFactory(); 233 | $view = m::mock(View::class); 234 | $view->shouldReceive('name')->once()->andReturn('name'); 235 | $factory->getDispatcher()->shouldReceive('dispatch')->once()->with('composing: name', [$view]); 236 | 237 | $factory->callComposer($view); 238 | } 239 | 240 | public function testComposersAreRegisteredWithSlashAndDot() 241 | { 242 | $factory = $this->getFactory(); 243 | $factory->getDispatcher()->shouldReceive('listen')->with('composing: foo.bar', m::any())->twice(); 244 | $factory->composer('foo.bar', ''); 245 | $factory->composer('foo/bar', ''); 246 | } 247 | 248 | public function testRenderCountHandling() 249 | { 250 | $factory = $this->getFactory(); 251 | $factory->incrementRender(); 252 | $this->assertFalse($factory->doneRendering()); 253 | $factory->decrementRender(); 254 | $this->assertTrue($factory->doneRendering()); 255 | } 256 | 257 | public function testYieldDefault() 258 | { 259 | $factory = $this->getFactory(); 260 | $this->assertEquals('hi', $factory->yieldContent('foo', 'hi')); 261 | } 262 | 263 | public function testYieldDefaultIsEscaped() 264 | { 265 | $factory = $this->getFactory(); 266 | $this->assertEquals('<p>hi</p>', $factory->yieldContent('foo', '

hi

')); 267 | } 268 | 269 | public function testYieldDefaultViewIsNotEscapedTwice() 270 | { 271 | $factory = $this->getFactory(); 272 | $view = m::mock(View::class); 273 | $view->shouldReceive('__toString')->once()->andReturn('

hi

<p>already escaped</p>'); 274 | $this->assertEquals('

hi

<p>already escaped</p>', $factory->yieldContent('foo', $view)); 275 | } 276 | 277 | public function testBasicSectionHandling() 278 | { 279 | $factory = $this->getFactory(); 280 | $factory->startSection('foo'); 281 | echo 'hi'; 282 | $factory->stopSection(); 283 | $this->assertEquals('hi', $factory->yieldContent('foo')); 284 | } 285 | 286 | public function testBasicSectionDefault() 287 | { 288 | $factory = $this->getFactory(); 289 | $factory->startSection('foo', 'hi'); 290 | $this->assertEquals('hi', $factory->yieldContent('foo')); 291 | } 292 | 293 | public function testBasicSectionDefaultIsEscaped() 294 | { 295 | $factory = $this->getFactory(); 296 | $factory->startSection('foo', '

hi

'); 297 | $this->assertEquals('<p>hi</p>', $factory->yieldContent('foo')); 298 | } 299 | 300 | public function testBasicSectionDefaultViewIsNotEscapedTwice() 301 | { 302 | $factory = $this->getFactory(); 303 | $view = m::mock(View::class); 304 | $view->shouldReceive('__toString')->once()->andReturn('

hi

<p>already escaped</p>'); 305 | $factory->startSection('foo', $view); 306 | $this->assertEquals('

hi

<p>already escaped</p>', $factory->yieldContent('foo')); 307 | } 308 | 309 | public function testSectionExtending() 310 | { 311 | $placeholder = Factory::parentPlaceholder('foo'); 312 | $factory = $this->getFactory(); 313 | $factory->startSection('foo'); 314 | echo 'hi '.$placeholder; 315 | $factory->stopSection(); 316 | $factory->startSection('foo'); 317 | echo 'there'; 318 | $factory->stopSection(); 319 | $this->assertEquals('hi there', $factory->yieldContent('foo')); 320 | } 321 | 322 | public function testSectionMultipleExtending() 323 | { 324 | $placeholder = Factory::parentPlaceholder('foo'); 325 | $factory = $this->getFactory(); 326 | $factory->startSection('foo'); 327 | echo 'hello '.$placeholder.' nice to see you '.$placeholder; 328 | $factory->stopSection(); 329 | $factory->startSection('foo'); 330 | echo 'my '.$placeholder; 331 | $factory->stopSection(); 332 | $factory->startSection('foo'); 333 | echo 'friend'; 334 | $factory->stopSection(); 335 | $this->assertEquals('hello my friend nice to see you my friend', $factory->yieldContent('foo')); 336 | } 337 | 338 | public function testComponentHandling() 339 | { 340 | $factory = $this->getFactory(); 341 | $factory->getFinder()->shouldReceive('find')->andReturn(__DIR__.'/fixtures/component.php'); 342 | $factory->getEngineResolver()->shouldReceive('resolve')->andReturn(new PhpEngine); 343 | $factory->getDispatcher()->shouldReceive('dispatch'); 344 | $factory->startComponent('component', ['name' => 'Taylor']); 345 | $factory->slot('title'); 346 | $factory->slot('website', 'laravel.com'); 347 | echo 'title
'; 348 | $factory->endSlot(); 349 | echo 'component'; 350 | $contents = $factory->renderComponent(); 351 | $this->assertEquals('title
component Taylor laravel.com', $contents); 352 | } 353 | 354 | public function testTranslation() 355 | { 356 | $container = new Container; 357 | $container->instance('translator', $translator = m::mock(stdClass::class)); 358 | $translator->shouldReceive('getFromJson')->with('Foo', ['name' => 'taylor'])->andReturn('Bar'); 359 | $factory = $this->getFactory(); 360 | $factory->setContainer($container); 361 | $factory->startTranslation(['name' => 'taylor']); 362 | echo 'Foo'; 363 | $string = $factory->renderTranslation(); 364 | 365 | $this->assertEquals('Bar', $string); 366 | } 367 | 368 | public function testSingleStackPush() 369 | { 370 | $factory = $this->getFactory(); 371 | $factory->startPush('foo'); 372 | echo 'hi'; 373 | $factory->stopPush(); 374 | $this->assertEquals('hi', $factory->yieldPushContent('foo')); 375 | } 376 | 377 | public function testMultipleStackPush() 378 | { 379 | $factory = $this->getFactory(); 380 | $factory->startPush('foo'); 381 | echo 'hi'; 382 | $factory->stopPush(); 383 | $factory->startPush('foo'); 384 | echo ', Hello!'; 385 | $factory->stopPush(); 386 | $this->assertEquals('hi, Hello!', $factory->yieldPushContent('foo')); 387 | } 388 | 389 | public function testSessionAppending() 390 | { 391 | $factory = $this->getFactory(); 392 | $factory->startSection('foo'); 393 | echo 'hi'; 394 | $factory->appendSection(); 395 | $factory->startSection('foo'); 396 | echo 'there'; 397 | $factory->appendSection(); 398 | $this->assertEquals('hithere', $factory->yieldContent('foo')); 399 | } 400 | 401 | public function testYieldSectionStopsAndYields() 402 | { 403 | $factory = $this->getFactory(); 404 | $factory->startSection('foo'); 405 | echo 'hi'; 406 | $this->assertEquals('hi', $factory->yieldSection()); 407 | } 408 | 409 | public function testInjectStartsSectionWithContent() 410 | { 411 | $factory = $this->getFactory(); 412 | $factory->inject('foo', 'hi'); 413 | $this->assertEquals('hi', $factory->yieldContent('foo')); 414 | } 415 | 416 | public function testEmptyStringIsReturnedForNonSections() 417 | { 418 | $factory = $this->getFactory(); 419 | $this->assertEmpty($factory->yieldContent('foo')); 420 | } 421 | 422 | public function testSectionFlushing() 423 | { 424 | $factory = $this->getFactory(); 425 | $factory->startSection('foo'); 426 | echo 'hi'; 427 | $factory->stopSection(); 428 | 429 | $this->assertCount(1, $factory->getSections()); 430 | 431 | $factory->flushSections(); 432 | 433 | $this->assertCount(0, $factory->getSections()); 434 | } 435 | 436 | public function testHasSection() 437 | { 438 | $factory = $this->getFactory(); 439 | $factory->startSection('foo'); 440 | echo 'hi'; 441 | $factory->stopSection(); 442 | 443 | $this->assertTrue($factory->hasSection('foo')); 444 | $this->assertFalse($factory->hasSection('bar')); 445 | } 446 | 447 | public function testGetSection() 448 | { 449 | $factory = $this->getFactory(); 450 | $factory->startSection('foo'); 451 | echo 'hi'; 452 | $factory->stopSection(); 453 | 454 | $this->assertEquals('hi', $factory->getSection('foo')); 455 | $this->assertNull($factory->getSection('bar')); 456 | $this->assertEquals('default', $factory->getSection('bar', 'default')); 457 | } 458 | 459 | public function testMakeWithSlashAndDot() 460 | { 461 | $factory = $this->getFactory(); 462 | $factory->getFinder()->shouldReceive('find')->twice()->with('foo.bar')->andReturn('path.php'); 463 | $factory->getEngineResolver()->shouldReceive('resolve')->twice()->with('php')->andReturn(m::mock(Engine::class)); 464 | $factory->getDispatcher()->shouldReceive('dispatch'); 465 | $factory->make('foo/bar'); 466 | $factory->make('foo.bar'); 467 | } 468 | 469 | public function testNamespacedViewNamesAreNormalizedProperly() 470 | { 471 | $factory = $this->getFactory(); 472 | $factory->getFinder()->shouldReceive('find')->twice()->with('vendor/package::foo.bar')->andReturn('path.php'); 473 | $factory->getEngineResolver()->shouldReceive('resolve')->twice()->with('php')->andReturn(m::mock(Engine::class)); 474 | $factory->getDispatcher()->shouldReceive('dispatch'); 475 | $factory->make('vendor/package::foo/bar'); 476 | $factory->make('vendor/package::foo.bar'); 477 | } 478 | 479 | public function testExceptionIsThrownForUnknownExtension() 480 | { 481 | $this->expectException(InvalidArgumentException::class); 482 | 483 | $factory = $this->getFactory(); 484 | $factory->getFinder()->shouldReceive('find')->once()->with('view')->andReturn('view.foo'); 485 | $factory->make('view'); 486 | } 487 | 488 | public function testExceptionsInSectionsAreThrown() 489 | { 490 | $this->expectException(ErrorException::class); 491 | $this->expectExceptionMessage('section exception message'); 492 | 493 | $engine = new CompilerEngine(m::mock(CompilerInterface::class)); 494 | $engine->getCompiler()->shouldReceive('getCompiledPath')->andReturnUsing(function ($path) { 495 | return $path; 496 | }); 497 | $engine->getCompiler()->shouldReceive('isExpired')->twice()->andReturn(false); 498 | $factory = $this->getFactory(); 499 | $factory->getEngineResolver()->shouldReceive('resolve')->twice()->andReturn($engine); 500 | $factory->getFinder()->shouldReceive('find')->once()->with('layout')->andReturn(__DIR__.'/fixtures/section-exception-layout.php'); 501 | $factory->getFinder()->shouldReceive('find')->once()->with('view')->andReturn(__DIR__.'/fixtures/section-exception.php'); 502 | $factory->getDispatcher()->shouldReceive('dispatch')->times(4); 503 | 504 | $factory->make('view')->render(); 505 | } 506 | 507 | public function testExtraStopSectionCallThrowsException() 508 | { 509 | $this->expectException(InvalidArgumentException::class); 510 | $this->expectExceptionMessage('Cannot end a section without first starting one.'); 511 | 512 | $factory = $this->getFactory(); 513 | $factory->startSection('foo'); 514 | $factory->stopSection(); 515 | 516 | $factory->stopSection(); 517 | } 518 | 519 | public function testExtraAppendSectionCallThrowsException() 520 | { 521 | $this->expectException(InvalidArgumentException::class); 522 | $this->expectExceptionMessage('Cannot end a section without first starting one.'); 523 | 524 | $factory = $this->getFactory(); 525 | $factory->startSection('foo'); 526 | $factory->stopSection(); 527 | 528 | $factory->appendSection(); 529 | } 530 | 531 | public function testAddingLoops() 532 | { 533 | $factory = $this->getFactory(); 534 | 535 | $factory->addLoop([1, 2, 3]); 536 | 537 | $expectedLoop = [ 538 | 'iteration' => 0, 539 | 'index' => 0, 540 | 'remaining' => 3, 541 | 'count' => 3, 542 | 'first' => true, 543 | 'last' => false, 544 | 'odd' => false, 545 | 'even' => true, 546 | 'depth' => 1, 547 | 'parent' => null, 548 | ]; 549 | 550 | $this->assertEquals([$expectedLoop], $factory->getLoopStack()); 551 | 552 | $factory->addLoop([1, 2, 3, 4]); 553 | 554 | $secondExpectedLoop = [ 555 | 'iteration' => 0, 556 | 'index' => 0, 557 | 'remaining' => 4, 558 | 'count' => 4, 559 | 'first' => true, 560 | 'last' => false, 561 | 'odd' => false, 562 | 'even' => true, 563 | 'depth' => 2, 564 | 'parent' => (object) $expectedLoop, 565 | ]; 566 | $this->assertEquals([$expectedLoop, $secondExpectedLoop], $factory->getLoopStack()); 567 | 568 | $factory->popLoop(); 569 | 570 | $this->assertEquals([$expectedLoop], $factory->getLoopStack()); 571 | } 572 | 573 | public function testAddingLoopDoesNotCloseGenerator() 574 | { 575 | $factory = $this->getFactory(); 576 | 577 | $data = (new class { 578 | public function generate() 579 | { 580 | for ($count = 0; $count < 3; $count++) { 581 | yield ['a', 'b']; 582 | } 583 | } 584 | })->generate(); 585 | 586 | $factory->addLoop($data); 587 | 588 | foreach ($data as $chunk) { 589 | $this->assertEquals(['a', 'b'], $chunk); 590 | } 591 | } 592 | 593 | public function testAddingUncountableLoop() 594 | { 595 | $factory = $this->getFactory(); 596 | 597 | $factory->addLoop(''); 598 | 599 | $expectedLoop = [ 600 | 'iteration' => 0, 601 | 'index' => 0, 602 | 'remaining' => null, 603 | 'count' => null, 604 | 'first' => true, 605 | 'last' => null, 606 | 'odd' => false, 607 | 'even' => true, 608 | 'depth' => 1, 609 | 'parent' => null, 610 | ]; 611 | 612 | $this->assertEquals([$expectedLoop], $factory->getLoopStack()); 613 | } 614 | 615 | public function testIncrementingLoopIndices() 616 | { 617 | $factory = $this->getFactory(); 618 | 619 | $factory->addLoop([1, 2, 3, 4]); 620 | 621 | $factory->incrementLoopIndices(); 622 | 623 | $this->assertEquals(1, $factory->getLoopStack()[0]['iteration']); 624 | $this->assertEquals(0, $factory->getLoopStack()[0]['index']); 625 | $this->assertEquals(3, $factory->getLoopStack()[0]['remaining']); 626 | $this->assertTrue($factory->getLoopStack()[0]['odd']); 627 | $this->assertFalse($factory->getLoopStack()[0]['even']); 628 | 629 | $factory->incrementLoopIndices(); 630 | 631 | $this->assertEquals(2, $factory->getLoopStack()[0]['iteration']); 632 | $this->assertEquals(1, $factory->getLoopStack()[0]['index']); 633 | $this->assertEquals(2, $factory->getLoopStack()[0]['remaining']); 634 | $this->assertFalse($factory->getLoopStack()[0]['odd']); 635 | $this->assertTrue($factory->getLoopStack()[0]['even']); 636 | } 637 | 638 | public function testReachingEndOfLoop() 639 | { 640 | $factory = $this->getFactory(); 641 | 642 | $factory->addLoop([1, 2]); 643 | 644 | $factory->incrementLoopIndices(); 645 | 646 | $factory->incrementLoopIndices(); 647 | 648 | $this->assertTrue($factory->getLoopStack()[0]['last']); 649 | } 650 | 651 | public function testIncrementingLoopIndicesOfUncountable() 652 | { 653 | $factory = $this->getFactory(); 654 | 655 | $factory->addLoop(''); 656 | 657 | $factory->incrementLoopIndices(); 658 | 659 | $factory->incrementLoopIndices(); 660 | 661 | $this->assertEquals(2, $factory->getLoopStack()[0]['iteration']); 662 | $this->assertEquals(1, $factory->getLoopStack()[0]['index']); 663 | $this->assertFalse($factory->getLoopStack()[0]['first']); 664 | $this->assertNull($factory->getLoopStack()[0]['remaining']); 665 | $this->assertNull($factory->getLoopStack()[0]['last']); 666 | } 667 | 668 | public function testMacro() 669 | { 670 | $factory = $this->getFactory(); 671 | $factory->macro('getFoo', function () { 672 | return 'Hello World'; 673 | }); 674 | $this->assertEquals('Hello World', $factory->getFoo()); 675 | } 676 | 677 | protected function getFactory() 678 | { 679 | return new Factory( 680 | m::mock(EngineResolver::class), 681 | m::mock(ViewFinderInterface::class), 682 | m::mock(DispatcherContract::class) 683 | ); 684 | } 685 | 686 | protected function getFactoryArgs() 687 | { 688 | return [ 689 | m::mock(EngineResolver::class), 690 | m::mock(ViewFinderInterface::class), 691 | m::mock(DispatcherContract::class), 692 | ]; 693 | } 694 | } 695 | --------------------------------------------------------------------------------