Package Data | |
---|---|
Maintainer Username: | delatbabel |
Maintainer Contact: | del@babel.com.au (Del) |
Package Create Date: | 2016-01-29 |
Package Last Update: | 2017-11-22 |
Language: | PHP |
License: | MIT |
Last Refreshed: | 2024-11-22 03:17:08 |
Package Statistics | |
---|---|
Total Downloads: | 1,378 |
Monthly Downloads: | 0 |
Daily Downloads: | 0 |
Total Stars: | 11 |
Total Watchers: | 4 |
Total Forks: | 4 |
Total Open Issues: | 1 |
Support view/rendering of Laravel pages and templates from a database.
Can be used for content management, admin interfaces (e.g. using AdminLTE or other front end frameworks), etc.
Supports loading both Blade and Twig templates using the same interface as existing view loading, for example:
return View::make("dashboard.sysadmin")
->with('page_title', 'System Administrator Dashboard')
->with('tasks', $tasks);
The lack of ability to have database backed views, templates, and layouts is one of the missing features that prevents Laravel from being used to create a truly dynamic CMS. This package aims to fix that.
TerrePorter partially solves this issue with his StringBladeCompiler package. His package was originally based on Flynsarmy/laravel-db-blade-compiler which did support taking a blade from a model object but is no longer maintained.
Add the package using composer from the command line:
composer require delatbabel/viewpages
Alternatively, pull the package in manually by adding these lines to your composer.json file:
"require": {
"delatbabel/viewpages": "~1.0"
},
Once that is done, run the composer update command:
composer update
After composer update completes, remove this line from your config/app.php file in the 'providers' array (or comment it out):
Illuminate\View\ViewServiceProvider::class
Replace it with this line:
Delatbabel\ViewPages\ViewPagesServiceProvider::class,
Finally, incorporate and run the migration scripts to create the database tables as follows:
php artisan vendor:publish --provider='Delatbabel\ViewPages\ViewPagesServiceProvider' --force
php artisan migrate
Prior to running the migration scripts you may want to alter the scripts themselves, or alter the base templates contained in database/seeds/examples. The ones provided are a few examples based on AdminLTE.
The important thing here is the pagekey. This basically takes the place of the view
name used to find the view in the existing Laravel View facade. So, for example, if you would
normally use View::make("dashboard.sysadmin");
to find the view, you would normally store
the view on disk in resources/views/dashboard/sysadmin.blade.php
. Instead you would store the
view in the vpages table as follows:
pagekey
= "dashboard.sysadmin"
url
= "dashboard/sysadmin"
name
= whatever you like as a name, e.g. "sysadmin dashboard"description
= whatever you like, e.g. "My system admin dashboard"content
= exactly the blade content that you would normally store on diskpagetype
= blade.php
.You can still use templates (layouts) as you normally would in Laravel. For example, your template can contain this:
<html>
<head><title>{{ $page_title }}</title></head>
<body>
@yield('body)
</body>
</html>
The body can then contain this:
@extends('layouts.main')
@section('body)
<p>Body text goes here</p>
@endsection
Store the template in the vpages table with pagekey = 'layouts.main' and it will automatically be found and extended by your body view.
See Template Inheritance for more details.
These are website dependent data blocks, stored in the vobjects table and retrieved using the VojbecService service which can be injected into a page using Laravel Service Injection.
Example:
@inject('objects', 'Delatbabel\ViewPages\Services\VobjectService')
<!-- Using the regular make method -->
<title> {{ $objects->make('page_title') }} </title>
<!-- Using a magic getter -->
<title> {{ $objects->page_title }} </title>
This package now supports views and templates using the Twig templating language as well as blade templates, via the TwigBridge class.
Store these in the database tables alongside your blade templates using pagetype
= "twig".
Once the templates are created, you can use them just like any other view file, e.g.
return view("dashboard.sysadmin")
->with('page_title', 'System Administrator Dashboard')
->with('tasks', $tasks);
Note that there's a bug in the Laravel View class where it uses its native factory in all cases, rather than fetching the factory from the application instance, so use the view() helper rather than the facade class like View::make().
The underlying Factory class will try to find the view by doing the following steps in order until a hit is found:
For the details on how this works, see later under "Architecture"
There is an included controller class called VpageController (which you are welcome to
extend) that can be used as a catch-all route. This controller contains one function
called index()
which simply loads a page from the database using the URL that was
provided. This gives you a simple blade based CMS for your application.
To include a route to this controller, include this route specification after all of the other routes in your routes file(s):
Route::any('{slug}', [
'as' => 'vpage.make',
'uses' => '\Delatbabel\ViewPages\Http\Controllers\VpageController@make'
])->where('slug', '.*');
You may need to add additional where clauses on this route to exclude other routes that your application consumes. e.g. to exclude all URLs under "/admin" and "/img" from this catch-all route, add the following where clause:
->where('slug', '^(?!admin)(?!img)([A-z\d-\/_.]+)?');
Vpage::make()
to restrict the view type pulled from the database.The original package that this was derived from had the idea of callouts. This meant that views could include calls like this:
{{ __o('toolbox@functionname') }}
__o was a helper function that called the Controller::call() function in Laravel 3 to render the output of the "functionname" action on the toolbox controller in a HMVC like manner. HMVC is really no longer supported by Laravel 5 (and Taylor thinks that HMVC is a bad idea, which I have to disagree with) so we need some other way of pulling in dynamic content. This will probably be via Repository or Service classes somehow.
Service Injection may already work, I haven't tested it.
I worked with a CMS system based on Laravel 3 that was fairly poor in its implementation, this package is designed to be a best practice implementation of what the Laravel 3 CMS was supposed to be.
The root of the View system is the Factory
class. This is accessed by the View
facade,
and a view
singleton which is registered in the application.
The only addition that I made to the Laravel's default Factory
class was to automatically
load the errors.410 view or then the errors.404 view if the requested view is not found. This
is mostly of use in CMS applications where views may be searched for by URL path, and a user
may enter a garbage URL so we want to put up a 404 page and not just an unhandled exception block.
The Factory
class contains an internal array called $extensions
which is a mapping from a
file extension (specifically a view path extension) to an engine ID. By default
the array looks like this:
protected $extensions = ['blade.php' => 'blade', 'php' => 'php'];
So the "blade.php" extension maps to the "blade" engine ID.
Additional engine extensions can be added to this $extensions
array by calling Factory::addExtension()
.
Note that this is already done by the TwigBridge service provider
The engine ID is passed to a resolver (EngineResolver
) which contains a mapping from the engine
ID to the engine itself. These mappings are created in the EngineResolver::resolve()
function.
So each time we return a blade template path from the database it needs to have a "blade.php" extension added to the template name so that the engine resolver can find the correct engine to compile and load the blade.
The Engine
class wraps up the functionality of loading and then compiling and then evaluating
the compiled template (with the data). This all happens in get()
, and for blade templates more
specifically (or any template that gets compiled to PHP) there is a function called evaluatePath()
inside the PhpEngine
class that includes the template directly and gets its output by wrapping
the include statement inside an ob_start()
and ob_end_clean()
pair (not efficient, which is
one of my reasons for disliking views that compile to PHP).
Evaluating a Twig template needs to be done differently to evaluating a Blade template because the
compiled result is not directly executable. Note that this is already done in the Engine in TwigBridge
.
The Laravel View system is somewhat backwards. The compilers each call the ViewFinders to find the files and then load the files themselves (in the compiler) rather than the file finding, loading and compiling (from string) happening independently. So this required somewhat of a re-implementation of the entire View system so that the loading stage and the compilation stage can happen separately.
This was done by:
VpageViewFinder
class which can determine whether a view is found in the database.ChainViewFinder
class which chained together the VpageViewFinder and the existing
Laravel FileViewFinder class (left untouched) to first pick the view from the database and
then fall back to the file system if it was not found there.EngineResolver
needs to have the extension added to the view name in order to be able to determine
the correct engine to hand the view to, so the VpageViewFinder
adds the file extension to the view name.
The Vpage
class which loads pages from the database can strip off this file extension before loading
the page in the make()
function.
Note that the extension doesn't have to be prefixed with ".", it can have any prefix. The Vpage
class defines the prefix to use as a constant.
Extending the compiler to include a loading stage was done by:
LoaderInterface
which defined the interface to a loader. The only function
that a loader needs is a get() function, to take the view name found by the finder and
load it as a string.FilesystemLoader
and VpageLoader
classes which can take the view name and load
it from the file system or database respectively.ChainLoader
class which can chain together the VpageLoader
and the FilesystemLoader
objects to load the view from wherever it happens to be found.The final step was to extend the native Laravel BladeCompiler
class to load the view contents
using one of the loaders (initialised as the ChainLoader
) instead of using its own internal
Filesystem
object to load the view contents. Compilation then happens as normal using the
compileString()
function.
The ChainViewFinder
class is also able to be used by the Twig
loader created in the TwigBridge
service provider, so that the twig loader's ViewFinder
can use the same approach to finding
views that the Blade loader uses.
Twig
(via TwigBridge
) already has the concept of a separate finder and loader class, however the
loader has to follow Twig's Twig_LoaderInterface
. To achieve this I built a VpageTwigLoader
class
to conform to the interface.
I over-rode the TwigBridge
ServiceProvider
class to provide a VpageTwigLoader
object in the
Twig_Loader_Chain
object that is already used to load twig data.
In modules/backend/twig
, OctoberCMS has a bunch of extensions
to the twig classes to do loading, etc. They have an implementaton of Twig_LoaderInterface
which
is still essentially a file based loader although it does a few extra Laravely things such as firing
events on load, and using its own CMS classes to do things like loading files (from disk or from
cache if they are present) using the Laravel File
and Cache
facades. This was not what I wanted
to do at all.
A view can be found by name or URL. A CMS may prefer to fetch views by URL, a system that
is just working on view names may prefer to fetch by page key (e.g. layouts.master
). The
Factory class attempts to find by key first, and then by URL.
If a view is not found in the database then a view by that key is searched for on disk.
If no view is found on the disk then the errors.410
and then the errors.404
views are searched
for in the database.
If no view is found at that point then an exception is thrown.
Compilation of blade templates is a bit of a black art, that's poorly explained in the Laravel
documentation. Basically, blade templates are all compiled to on-disk PHP files which are
then stored in storage/framework/views
. Once a compiled version of a template goes out of date
it is replaced with a newer copy. The caching of these compiled templates normally depends on
the file date of the blade template file, however in this extension we make it depend on the
updated_at
date of the template data in the database.
When a blade is compiled to PHP, the directives are compiled as follows:
These go together. @extends compiles to:
echo $__env->make('layout.name', array_except(get_defined_vars(), array('__data', '__path')))->render();
@section and @endsecton compile to:
$__env->startSection('section_name');
$__env->stopSection();
Note that the directives appear in the compiled file in the opposite order to which they appear in the blade template file -- normally in the template file @extends would be at the top and @section / @endsection would be below, in the compiled template file the compiled version of @extends is at the end of the file.
@yield('section_name') appears like this in the compiled file:
echo $__env->yieldContent('body');
The global variable $__env
is actually an instance of Illuminate\View\Factory
.
That class implements the necessary make()
, startSection()
, stopSection()
and yieldContent()
functions, which make the content appear in the correct place.
The critical function is make()
which does this:
View
objectThe problem with the base Factory
class is that it assumes that the view name is a file
name that has an extension, and the "php" or "blade.php" or "twig" extensions can be
identified to determine the view type. Instead I store the view type in the database
table.
The finder and loader have been extended so that they are able to pull the
view from the database instead of from disk. The logic of pulling the view from the
database is all in the Vpage::make()
function.
Once the content of the blade is pulled from the database and the engine is resolved
then the view factory and the engine are then passed by Factory::make()
to the view.
View::render()
does the actual rendering by:
View::renderContents()
View::getContents()
Engine::get()
Engine::get()
calls Compiler::compile()
to compile the view if the cached copy of the compiled view is expiredCompiler::getCompiledPath()
which returns the path of the compiled viewEngine::evaluatePath()
which passes the data to the compiled viewThe service provider here is fairly simple -- however there are 3:
ViewPagesServiceProvider
-- does the normal registration of migrations, seeds, and also calls
in StringBladeCompilerServiceProvider
.IlluminateViewServiceProvider
-- extends the base Laravel View Service Provider to include the
necessary additional finder and loader classes.TwigBridgeServiceProvider
-- extends the Service Provider in TwigBridge
so that the twig chain loader includes
a class to load twig templates from the database.The model class (Vpage
) replaces the on-disk storage of template files, so that the Factory
class discussed above can pull templates from the database rather than disk.