October is a Laravel-based, pre-built CMS which was recently announced. I have yet to see the code powering what looks like a beautiful and efficient CMS system. So I thought I would try to implement some of the concepts presented in the introductory video as they illustrate valuable tips for working with Laravel.
We’re developing a Laravel 4 application which has lots of server-side aspects; but there’s also an interactive interface. There be scripts!
For this; we’re using Bootstrap and jQuery. Download Bootstrap at: http://getbootstrap.com/ and unpack it into your public folder. Where you put the individual files makes little difference, but I have put the scripts in public/js, the stylesheets in public/css and the fonts in public/fonts. Where you see those paths in my source code; you should substitute them with your own.
Next up, download jQuery at: http://jquery.com/download and unpack it into your public folder.
On the server-side, we’re going to be using Flysystem for reading and writing files. Add it to the Composer dependencies:
1
"require"
:
{
2
"laravel/framework"
:
"4.1.*"
,
3
"league/flysystem"
:
"0.2.*"
4
},
Follow that up with:
1
composer update
We’ve often used View::make() to render views. It’s great for when we have pre-defined view files and we want Laravel to manage how they are rendered and stored. In this tutorial, we’re going to be rendering templates from strings. We’ll need to encapsulate some of how Laravel rendered templates, but it’ll also give us a good base for extending upon the Blade template syntax.
Let’s get started by creating a service provider:
1
php artisan workbench formativ/cms
This will generate the usual scaffolding for a new package. We need to add it in a few places, to be able to use it in our application:
1
"providers"
=>
[
2
"Formativ\Cms\CmsServiceProvider"
,
3
// …remaining service providers
4
],
1
"autoload"
:
{
2
"classmap"
:
[
3
// …
4
],
5
"psr-0"
:
{
6
"Formativ\\Cms"
:
"workbench/formativ/cms/src/"
7
}
8
}
Then we need to rebuild the composer autoloader:
1
composer dump-autoload
All this gets us to a place where we can start to add classes for encapsulating and extending Blade rendering. Let’s create some wrapper classes, and register them in the service provider:
1
<?
php
2
3
namespace
Formativ\Cms
;
4
5
interface
CompilerInterface
6
{
7
public
function
compileString
(
$template
);
8
}
1
<?
php
2
3
namespace
Formativ\Cms\Compiler
;
4
5
use
Formativ\Cms\CompilerInterface
;
6
use
Illuminate\View\Compilers\BladeCompiler
;
7
8
class
Blade
9
extends
BladeCompiler
10
implements
CompilerInterface
11
{
12
13
}
1
<?
php
2
3
namespace
Formativ\Cms
;
4
5
interface
EngineInterface
6
{
7
public
function
render
(
$template
,
$data
);
8
}
1
<?
php
2
3
namespace
Formativ\Cms\Engine
;
4
5
use
Formativ\Cms\CompilerInterface
;
6
use
Formativ\Cms\EngineInterface
;
7
8
class
Blade
9
implements
EngineInterface
10
{
11
protected
$compiler
;
12
13
public
function
__construct
(
CompilerInterface
$compiler
)
14
{
15
$this
->
compiler
=
$compiler
;
16
}
17
18
public
function
render
(
$template
,
$data
)
19
{
20
$compiled
=
$this
->
compiler
->
compileString
(
$template
);
21
22
ob_start
();
23
extract
(
$data
,
EXTR_SKIP
);
24
25
try
26
{
27
eval
(
"?>"
.
$compiled
);
28
}
29
catch
(
Exception
$e
)
30
{
31
ob_end_clean
();
32
throw
$e
;
33
}
34
35
$result
=
ob_get_contents
();
36
ob_end_clean
();
37
38
return
$result
;
39
}
40
}
1
<?
php
2
3
namespace
Formativ\Cms
;
4
5
use
Illuminate\Support\ServiceProvider
;
6
7
class
CmsServiceProvider
8
extends
ServiceProvider
9
{
10
protected
$defer
=
true
;
11
12
public
function
register
()
13
{
14
$this
->
app
->
bind
(
15
"Formativ\Cms\CompilerInterface"
,
16
function
()
{
17
return
new
Compiler\Blade
(
18
$this
->
app
->
make
(
"files"
),
19
$this
->
app
->
make
(
"path.storage"
)
.
"/views"
20
);
21
}
22
);
23
24
$this
->
app
->
bind
(
25
"Formativ\Cms\EngineInterface"
,
26
"Formativ\Cms\Engine\Blade"
27
);
28
}
29
30
public
function
provides
()
31
{
32
return
[
33
"Formativ\Cms\CompilerInterface"
,
34
"Formativ\Cms\EngineInterface"
35
];
36
}
37
}
The CompilerBlade class encapsulates the BladeCompiler class, allowing us to implement the CompilerInterface interface. This is a way of future-proofing our package so that the code which depends on methods Blade currently implements won’t fail if future versions of Blade were to remove that implementation.
The EngineBlade class contains the method which we will use to render template strings. It implements the EngineInterface interface for that same future-proofing.
We register all of these in the CmsServiceProvider. We can now inject these dependencies in our controller:
1
<?
php
2
3
use
Formativ\Cms\EngineInterface
;
4
5
class
IndexController
6
extends
BaseController
7
{
8
protected
$engine
;
9
10
public
function
__construct
(
EngineInterface
$engine
)
11
{
12
$this
->
engine
=
$engine
;
13
}
14
15
public
function
indexAction
()
16
{
17
// ...use $this->engine->render() here
18
}
19
}
As we bound FormativCmsEngineInterface in our service provider, we can now specify it in our controller constructor and Laravel will automatically inject it for us.
One of the interesting things October does is store all of the page and layout meta data at the top of the template file. This allows changes to metadata (which would normally take place elsewhere) to be version-controlled. Having gained the ability to render template strings, we’re now in a position to be able to isolate this kind of metadata and render the rest of the file as a template.
Consider the following example:
1
protected
function
minify
(
$html
)
2
{
3
$search
=
array
(
4
"/\>[^\S ]+/s"
,
5
"/[^\S ]+\</s"
,
6
"/(\s)+/s"
7
);
8
9
$replace
=
array
(
10
">"
,
11
"<"
,
12
"
\\
1"
13
);
14
15
$html
=
preg_replace
(
$search
,
$replace
,
$html
);
16
17
return
$html
;
18
}
19
20
public
function
indexAction
()
21
{
22
$template
=
$this
->
minify
(
"
23
<!doctype html>
24
<html lang='en'>
25
<head>
26
<title>
27
Laravel 4 File-Based CMS
28
</title>
29
</head>
30
<body>
31
Hello world
32
</body>
33
</html>
34
"
);
35
36
return
$this
->
engine
->
render
(
$template
,
[]);
37
}
Here we’re rendering a page template (with the help of a minify method). It’s just like what we did before. Let’s add some metadata, and pull it out of the template before rendering:
1
protected
function
extractMeta
(
$html
)
2
{
3
$parts
=
explode
(
"=="
,
$html
,
2
);
4
5
$meta
=
""
;
6
$html
=
$parts
[
0
];
7
8
if
(
count
(
$parts
)
>
1
)
9
{
10
$meta
=
$parts
[
0
];
11
$html
=
$parts
[
1
];
12
}
13
14
return
[
15
"meta"
=>
$meta
,
16
"html"
=>
$html
17
];
18
}
19
20
protected
function
parseMeta
(
$meta
)
21
{
22
$meta
=
trim
(
$meta
);
23
$lines
=
explode
(
"
\n
"
,
$meta
);
24
$data
=
[];
25
26
foreach
(
$lines
as
$line
)
27
{
28
$parts
=
explode
(
"="
,
$line
);
29
$data
[
trim
(
$parts
[
0
])]
=
trim
(
$parts
[
1
]);
30
}
31
32
return
$data
;
33
}
34
35
public
function
indexAction
()
36
{
37
$parts
=
$this
->
extractMeta
(
"
38
title = Laravel 4 File-Based CMS
39
message = Hello world
40
==
41
<!doctype html>
42
<html lang='en'>
43
<head>
44
<title>
45
{{
\$
title }}
46
</title>
47
</head>
48
<body>
49
{{
\$
message }}
50
</body>
51
</html>
52
"
);
53
54
$data
=
$this
->
parseMeta
(
$parts
[
"meta"
]);
55
$template
=
$this
->
minify
(
$parts
[
"html"
]);
56
57
return
$this
->
engine
->
render
(
$template
,
$data
);
58
}
This time round, we’re using an extractMeta() method to pull the meta data string out of the template string, and a parseMeta() method to split the lines of metadata into key/value pairs.
The result is a functional means of storing and parsing meta data, and rendering the remaining template from and to a string.
We need to create some sort of admin interface, with which to create and/or modify pages and layouts. Let’s skip the authentication system (as we’ve done that before and it will distract from the focus of this tutorial).
I’ve chosen for us to use Flysystem when working with the filesystem. It would be a good idea to future-proof this dependency by wrapping it in a subclass which implements an interface we control.
1
<?
php
2
3
namespace
Formativ\Cms
;
4
5
interface
FilesystemInterface
6
{
7
public
function
has
(
$file
);
8
public
function
listContents
(
$folder
,
$detail
=
false
);
9
public
function
write
(
$file
,
$contents
);
10
public
function
read
(
$file
);
11
public
function
put
(
$file
,
$content
);
12
public
function
delete
(
$file
);
13
}
1
<?
php
2
3
namespace
Formativ\Cms
;
4
5
use
League\Flysystem\Filesystem
as
Base
;
6
7
class
Filesystem
8
extends
Base
9
implements
FilesystemInterface
10
{
11
12
}
1
public
function
register
()
2
{
3
$this
->
app
->
bind
(
4
"Formativ\Cms\CompilerInterface"
,
5
function
()
{
6
return
new
Compiler\Blade
(
7
$this
->
app
->
make
(
"files"
),
8
$this
->
app
->
make
(
"path.storage"
)
.
"/views"
9
);
10
}
11
);
12
13
$this
->
app
->
bind
(
14
"Formativ\Cms\EngineInterface"
,
15
"Formativ\Cms\Engine\Blade"
16
);
17
18
$this
->
app
->
bind
(
19
"Formativ\Cms\FilesystemInterface"
,
20
function
()
{
21
return
new
Filesystem
(
22
new
Local
(
23
$this
->
app
->
make
(
"path.base"
)
.
"/app/views"
24
)
25
);
26
}
27
);
28
}
We’re not really adding any extra functionality to that which Flysystem provides. The sole purpose of us wrapping the Local Flysystem adapter is to make provision for swapping it with another filesystem class/library.
We should also move the metadata-related functionality into a better location.
1
<?
php
2
3
namespace
Formativ\Cms
;
4
5
interface
EngineInterface
6
{
7
public
function
render
(
$template
,
$data
);
8
public
function
extractMeta
(
$template
);
9
public
function
parseMeta
(
$meta
);
10
public
function
minify
(
$template
);
11
}
1
<?
php
2
3
namespace
Formativ\Cms\Engine
;
4
5
use
Formativ\Cms\CompilerInterface
;
6
use
Formativ\Cms\EngineInterface
;
7
8
class
Blade
9
implements
EngineInterface
10
{
11
protected
$compiler
;
12
13
public
function
__construct
(
CompilerInterface
$compiler
)
14
{
15
$this
->
compiler
=
$compiler
;
16
}
17
18
public
function
render
(
$template
,
$data
)
19
{
20
$extracted
=
$this
->
extractMeta
(
$template
)[
"template"
];
21
$compiled
=
$this
->
compiler
->
compileString
(
$extracted
);
22
23
ob_start
();
24
extract
(
$data
,
EXTR_SKIP
);
25
26
try
27
{
28
eval
(
"?>"
.
$compiled
);
29
}
30
catch
(
Exception
$e
)
31
{
32
ob_end_clean
();
33
throw
$e
;
34
}
35
36
$result
=
ob_get_contents
();
37
ob_end_clean
();
38
39
return
$result
;
40
}
41
42
public
function
minify
(
$template
)
43
{
44
$search
=
array
(
45
"/\>[^\S ]+/s"
,
46
"/[^\S ]+\</s"
,
47
"/(\s)+/s"
48
);
49
50
$replace
=
array
(
51
">"
,
52
"<"
,
53
"
\\
1"
54
);
55
56
$template
=
preg_replace
(
$search
,
$replace
,
$template
);
57
58
return
$template
;
59
}
60
61
public
function
extractMeta
(
$template
)
62
{
63
$parts
=
explode
(
"=="
,
$template
,
2
);
64
65
$meta
=
""
;
66
$template
=
$parts
[
0
];
67
68
if
(
count
(
$parts
)
>
1
)
69
{
70
$meta
=
$parts
[
0
];
71
$template
=
$parts
[
1
];
72
}
73
74
return
[
75
"meta"
=>
$meta
,
76
"template"
=>
$template
77
];
78
}
79
80
public
function
parseMeta
(
$meta
)
81
{
82
$meta
=
trim
(
$meta
);
83
$lines
=
explode
(
"
\n
"
,
$meta
);
84
$data
=
[];
85
86
foreach
(
$lines
as
$line
)
87
{
88
$parts
=
explode
(
"="
,
$line
);
89
$data
[
trim
(
$parts
[
0
])]
=
trim
(
$parts
[
1
]);
90
}
91
92
return
$data
;
93
}
94
}
The only difference can be found in the argument names (to bring them more in line with the rest of the class) and integrating the meta methods into the render() method.
Next up is layout controller class:
1
<?
php
2
3
use
Formativ\Cms\EngineInterface
;
4
use
Formativ\Cms\FilesystemInterface
;
5
6
class
LayoutController
7
extends
BaseController
8
{
9
protected
$engine
;
10
protected
$filesystem
;
11
12
public
function
__construct
(
13
EngineInterface
$engine
,
14
FilesystemInterface
$filesystem
15
)
16
{
17
$this
->
engine
=
$engine
;
18
$this
->
filesystem
=
$filesystem
;
19
20
Validator
::
extend
(
21
"add"
,
22
function
(
$attribute
,
$value
,
$params
)
{
23
return
!
$this
->
filesystem
->
has
(
"layouts/"
.
$value
);
24
}
25
);
26
27
Validator
::
extend
(
28
"edit"
,
29
function
(
$attribute
,
$value
,
$params
)
{
30
$new
=
!
$this
->
filesystem
->
has
(
"layouts/"
.
$value
);
31
$same
=
$this
->
filesystem
->
has
(
"layouts/"
.
$params
[
0
]);
32
33
return
$new
or
$same
;
34
}
35
);
36
}
37
38
public
function
indexAction
()
39
{
40
$layouts
=
$this
->
filesystem
->
listContents
(
"layouts"
);
41
$edit
=
URL
::
route
(
"admin/layout/edit"
)
.
"?layout="
;
42
$delete
=
URL
::
route
(
"admin/layout/delete"
)
.
"?layout="
;
43
44
return
View
::
make
(
"admin/layout/index"
,
compact
(
45
"layouts"
,
46
"edit"
,
47
"delete"
48
));
49
}
50
}
This is the first time we’re using Dependency Injection in our controller. Laravel is injecting our engine interface (which is the Blade wrapper) and our filesystem interface (which is the Flysystem wrapper). As usual, we assign the injected dependencies to protected properties. We also define two custom validation rules, which we’ll use when adding and editing the layout files.
We’ve also defined an indexAction() method which will be used to display a list of layout files which can then be edited or deleted. For the interface to be complete, we are going to need the following files:
1
<!doctype html>
2
<html
lang=
"en"
>
3
<head>
4
<meta
charset=
"utf-8"
/>
5
<title>
Laravel 4 File-Based CMS</title>
6
<link
7
rel=
"stylesheet"
8
href=
"{{ asset("
css
/
bootstrap
.
min
.
css
");
}}"
9
/>
10
<link
11
rel=
"stylesheet"
12
href=
"{{ asset("
css
/
shared
.
css
");
}}"
13
/>
14
</head>
15
<body>
16
@include("admin/include/navigation") 17
<div
class=
"container"
>
18
<div
class=
"row"
>
19
<div
class=
"column md-12"
>
20
@yield("content") 21
</div>
22
</div>
23
</div>
24
<script
src=
"{{ asset("
js
/
jquery
.
min
.
js
");
}}"
></script>
25
<script
src=
"{{ asset("
js
/
bootstrap
.
min
.
js
");
}}"
></script>
26
</body>
27
</html>
1
<nav
2
class=
"navbar navbar-inverse navbar-fixed-top"
3
role=
"navigation"
4
>
5
<div
class=
"container-fluid"
>
6
<div
class=
"navbar-header"
>
7
<button
type=
"button"
8
class=
"navbar-toggle"
9
data-toggle=
"collapse"
10
data-target=
"#navbar-collapse"
11
>
12
<span
class=
"sr-only"
>
Toggle navigation</span>
13
<span
class=
"icon-bar"
></span>
14
<span
class=
"icon-bar"
></span>
15
<span
class=
"icon-bar"
></span>
16
</button>
17
</div>
18
<div
19
class=
"collapse navbar-collapse"
20
id=
"navbar-collapse"
21
>
22
<ul
class=
"nav navbar-nav"
>
23
<li
class=
"@yield("
navigation
/
layout
/
class
")"
>
24
<a
href=
"{{ URL::route("
admin
/
layout
/
index
")
}}"
>
25
Layouts 26
</a>
27
</li>
28
</div>
29
</div>
30
</nav>
1
<ol
class=
"breadcrumb"
>
2
<li>
3
<a
href=
"{{ URL::route("
admin
/
layout
/
index
")
}}"
>
4
List Layouts 5
</a>
6
</li>
7
<li>
8
<a
href=
"{{ URL::route("
admin
/
layout
/
add
")
}}"
>
9
Add New Layout 10
</a>
11
</li>
12
</ol>
1
@extends("admin/layout") 2
@section("navigation/layout/class") 3
active 4
@stop 5
@section("content") 6
@include("admin/include/layout/navigation") 7
@if (count($layouts)) 8
<table
class=
"table table-striped"
>
9
<thead>
10
<tr>
11
<th
class=
"wide"
>
12
File 13
</th>
14
<th
class=
"narrow"
>
15
Actions 16
</th>
17
</tr>
18
</thead>
19
<tbody>
20
@foreach ($layouts as $layout) 21
@if ($layout["type"] == "file") 22
<tr>
23
<td
class=
"wide"
>
24
<a
href=
"{{ $edit . $layout["
basename
"]
}}"
>
25
{{ $layout["basename"] }} 26
</a>
27
</td>
28
<td
class=
"narrow actions"
>
29
<a
href=
"{{ $edit . $layout["
basename
"]
}}"
>
30
<i
class=
"glyphicon glyphicon-pencil"
></i>
31
</a>
32
<a
href=
"{{ $delete . $layout["
basename
"]
}}"
>
33
<i
class=
"glyphicon glyphicon-trash"
></i>
34
</a>
35
</td>
36
</tr>
37
@endif 38
@endforeach 39
</tbody>
40
</table>
41
@else 42
No layouts yet. 43
<a
href=
"{{ URL::route("
admin
/
layout
/
add
")
}}"
>
44
create one now! 45
</a>
46
@endif 47
@stop
1
Route
::
any
(
"admin/layout/index"
,
[
2
"as"
=>
"admin/layout/index"
,
3
"uses"
=>
"LayoutController@indexAction"
4
]);
These are quite a few files, so let’s go over them individually:
Let’s move onto the layout add page:
1
public
function
addAction
()
2
{
3
if
(
Input
::
has
(
"save"
))
4
{
5
$validator
=
Validator
::
make
(
Input
::
all
(),
[
6
"name"
=>
"required|add"
,
7
"code"
=>
"required"
8
]);
9
10
if
(
$validator
->
fails
())
11
{
12
return
Redirect
::
route
(
"admin/layout/add"
)
13
->
withInput
()
14
->
withErrors
(
$validator
);
15
}
16
17
$meta
=
"
18
title = "
.
Input
::
get
(
"title"
)
.
"
19
description = "
.
Input
::
get
(
"description"
)
.
"
20
==
21
"
;
22
23
$name
=
"layouts/"
.
Input
::
get
(
"name"
)
.
".blade.php"
;
24
25
$this
->
filesystem
->
write
(
$name
,
$meta
.
Input
::
get
(
"code"
));
26
27
return
Redirect
::
route
(
"admin/layout/index"
);
28
}
29
30
return
View
::
make
(
"admin/layout/add"
);
31
}
1
@extends("admin/layout") 2
@section("navigation/layout/class") 3
active 4
@stop 5
@section("content") 6
@include("admin/include/layout/navigation") 7
<form
role=
"form"
method=
"post"
>
8
<div
class=
"form-group"
>
9
<label
for=
"name"
>
Name</label>
10
<span
class=
"help-text text-danger"
>
11
{{ $errors->first("name") }} 12
</span>
13
<input
14
type=
"text"
15
class=
"form-control"
16
id=
"name"
17
name=
"name"
18
placeholder=
"new-layout"
19
value=
"{{ Input::old("
name
")
}}"
20
/>
21
</div>
22
<div
class=
"form-group"
>
23
<label
for=
"title"
>
Meta Title</label>
24
<input
25
type=
"text"
26
class=
"form-control"
27
id=
"title"
28
name=
"title"
29
value=
"{{ Input::old("
title
")
}}"
30
/>
31
</div>
32
<div
class=
"form-group"
>
33
<label
for=
"description"
>
Meta Description</label>
34
<input
35
type=
"text"
36
class=
"form-control"
37
id=
"description"
38
name=
"description"
39
value=
"{{ Input::old("
description
")
}}"
40
/>
41
</div>
42
<div
class=
"form-group"
>
43
<label
for=
"code"
>
Code</label>
44
<span
class=
"help-text text-danger"
>
45
{{ $errors->first("code") }} 46
</span>
47
<textarea
48
class=
"form-control"
49
id=
"code"
50
name=
"code"
51
rows=
"5"
52
placeholder=
"<div>Hello world</div>"
53
>
{{ Input::old("code") }}</textarea>
54
</div>
55
<input
56
type=
"submit"
57
name=
"save"
58
class=
"btn btn-default"
59
value=
"Save"
60
/>
61
</form>
62
@stop
1
Route
::
any
(
"admin/layout/add"
,
[
2
"as"
=>
"admin/layout/add"
,
3
"uses"
=>
"LayoutController@addAction"
4
]);
The form processing, in the addAction() method, is wrapped in a check for the save parameter. This is the name of the submit button on the add form. We specify the validation rules (including one of those we defined in the constructor). If validation fails, we redirect back to the add page, bringing along the errors and old input. If not, we create a new file with the default meta title and default meta description as metadata. Finally we redirect to the index page.
The view is fairly standard (including the bootstrap tags we’ve used). The name and code fields have error messages and all of the fields have their values set to the old input values. We’ve also added a route to the add page.
Edit follows a similar pattern:
1
public
function
editAction
()
2
{
3
$layout
=
Input
::
get
(
"layout"
);
4
$name
=
str_ireplace
(
".blade.php"
,
""
,
$layout
);
5
$content
=
$this
->
filesystem
->
read
(
"layouts/"
.
$layout
);
6
$extracted
=
$this
->
engine
->
extractMeta
(
$content
);
7
$code
=
trim
(
$extracted
[
"template"
]);
8
$parsed
=
$this
->
engine
->
parseMeta
(
$extracted
[
"meta"
]);
9
$title
=
$parsed
[
"title"
];
10
$description
=
$parsed
[
"description"
];
11
12
if
(
Input
::
has
(
"save"
))
13
{
14
$validator
=
Validator
::
make
(
Input
::
all
(),
[
15
"name"
=>
"required|edit:"
.
Input
::
get
(
"layout"
),
16
"code"
=>
"required"
17
]);
18
19
if
(
$validator
->
fails
())
20
{
21
return
Redirect
::
route
(
"admin/layout/edit"
)
22
->
withInput
()
23
->
withErrors
(
$validator
);
24
}
25
26
$meta
=
"
27
title = "
.
Input
::
get
(
"title"
)
.
"
28
description = "
.
Input
::
get
(
"description"
)
.
"
29
==
30
"
;
31
32
$name
=
"layouts/"
.
Input
::
get
(
"name"
)
.
".blade.php"
;
33
34
$this
->
filesystem
->
put
(
$name
,
$meta
.
Input
::
get
(
"code"
));
35
36
return
Redirect
::
route
(
"admin/layout/index"
);
37
}
38
39
return
View
::
make
(
"admin/layout/edit"
,
compact
(
40
"name"
,
41
"title"
,
42
"description"
,
43
"code"
44
));
45
}
1
@extends("admin/layout") 2
@section("navigation/layout/class") 3
active 4
@stop 5
@section("content") 6
@include("admin/include/layout/navigation") 7
<form
role=
"form"
method=
"post"
>
8
<div
class=
"form-group"
>
9
<label
for=
"name"
>
Name</label>
10
<span
class=
"help-text text-danger"
>
11
{{ $errors->first("name") }} 12
</span>
13
<input
14
type=
"text"
15
class=
"form-control"
16
id=
"name"
17
name=
"name"
18
placeholder=
"new-layout"
19
value=
"{{ Input::old("
name
",
$
name
)
}}"
20
/>
21
</div>
22
<div
class=
"form-group"
>
23
<label
for=
"title"
>
Meta Title</label>
24
<input
25
type=
"text"
26
class=
"form-control"
27
id=
"title"
28
name=
"title"
29
value=
"{{ Input::old("
title
",
$
title
)
}}"
30
/>
31
</div>
32
<div
class=
"form-group"
>
33
<label
for=
"description"
>
Meta Description</label>
34
<input
35
type=
"text"
36
class=
"form-control"
37
id=
"description"
38
name=
"description"
39
value=
"{{ Input::old("
description
",
$
description
)
}}"
40
/>
41
</div>
42
<div
class=
"form-group"
>
43
<label
for=
"code"
>
Code</label>
44
<span
class=
"help-text text-danger"
>
45
{{ $errors->first("code") }} 46
</span>
47
<textarea
48
class=
"form-control"
49
id=
"code"
50
name=
"code"
51
rows=
"5"
52
placeholder=
"<div>Hello world</div>"
53
>
{{ Input::old("code", $code) }}</textarea>
54
</div>
55
<input
56
type=
"submit"
57
name=
"save"
58
class=
"btn btn-default"
59
value=
"Save"
60
/>
61
</form>
62
@stop
1
Route
::
any
(
"admin/layout/edit"
,
[
2
"as"
=>
"admin/layout/edit"
,
3
"uses"
=>
"LayoutController@editAction"
4
]);
The editAction() method fetches the layout file data and extracts/parses the metadata, so that we can present it in the edit form. Other than utilising the second custom validation function, we define in the constructor, there’s nothing else noteworthy in this method.
The edit form is also pretty much the same, except that we provide default values to the Input::old() method calls, giving the data extracted from the layout file. We also add a route to the edit page.
Deleting layout files is even simpler:
1
public
function
deleteAction
()
2
{
3
$name
=
"layouts/"
.
Input
::
get
(
"layout"
);
4
$this
->
filesystem
->
delete
(
$name
);
5
6
return
Redirect
::
route
(
"admin/layout/index"
);
7
}
1
Route
::
any
(
"admin/layout/delete"
,
[
2
"as"
=>
"admin/layout/delete"
,
3
"uses"
=>
"LayoutController@deleteAction"
4
]);
We link straight to the deleteAction() method in the index view. This method simply deletes the layout file and redirects back to the index page. We’ve added the appropriate route to make this page accessible.
We can now list the layout files, add new ones, edit existing ones and delete those layout files we no longer require. It’s basic, and could definitely be polished a bit, but it’s sufficient for our needs.
Pages are handled in much the same way, so we’re not going to spend too much time on them. Let’s begin with the controller:
1
<?
php
2
3
use
Formativ\Cms\EngineInterface
;
4
use
Formativ\Cms\FilesystemInterface
;
5
6
class
PageController
7
extends
BaseController
8
{
9
protected
$engine
;
10
protected
$filesystem
;
11
12
public
function
__construct
(
13
EngineInterface
$engine
,
14
FilesystemInterface
$filesystem
15
)
16
{
17
$this
->
engine
=
$engine
;
18
$this
->
filesystem
=
$filesystem
;
19
20
Validator
::
extend
(
21
"add"
,
22
function
(
$attribute
,
$value
,
$params
)
{
23
return
!
$this
->
filesystem
->
has
(
"pages/"
.
$value
);
24
}
25
);
26
27
Validator
::
extend
(
28
"edit"
,
29
function
(
$attribute
,
$value
,
$params
)
{
30
$new
=
!
$this
->
filesystem
->
has
(
"pages/"
.
$value
);
31
$same
=
$this
->
filesystem
->
has
(
"pages/"
.
$params
[
0
]);
32
33
return
$new
or
$same
;
34
}
35
);
36
}
37
38
public
function
indexAction
()
39
{
40
$pages
=
$this
->
filesystem
->
listContents
(
"pages"
);
41
$edit
=
URL
::
route
(
"admin/page/edit"
)
.
"?page="
;
42
$delete
=
URL
::
route
(
"admin/page/delete"
)
.
"?page="
;
43
44
return
View
::
make
(
"admin/page/index"
,
compact
(
45
"pages"
,
46
"edit"
,
47
"delete"
48
));
49
}
50
51
public
function
addAction
()
52
{
53
$files
=
$this
->
filesystem
->
listContents
(
"layouts"
);
54
$layouts
=
[];
55
56
foreach
(
$files
as
$file
)
57
{
58
$name
=
$file
[
"basename"
];
59
$layouts
[
$name
]
=
$name
;
60
}
61
62
if
(
Input
::
has
(
"save"
))
63
{
64
$validator
=
Validator
::
make
(
Input
::
all
(),
[
65
"name"
=>
"required|add"
,
66
"route"
=>
"required"
,
67
"layout"
=>
"required"
,
68
"code"
=>
"required"
69
]);
70
71
if
(
$validator
->
fails
())
72
{
73
return
Redirect
::
route
(
"admin/page/add"
)
74
->
withInput
()
75
->
withErrors
(
$validator
);
76
}
77
78
$meta
=
"
79
title = "
.
Input
::
get
(
"title"
)
.
"
80
description = "
.
Input
::
get
(
"description"
)
.
"
81
layout = "
.
Input
::
get
(
"layout"
)
.
"
82
route = "
.
Input
::
get
(
"route"
)
.
"
83
==
84
"
;
85
86
$name
=
"pages/"
.
Input
::
get
(
"name"
)
.
".blade.php"
;
87
$code
=
$meta
.
Input
::
get
(
"code"
);
88
89
$this
->
filesystem
->
write
(
$name
,
$code
);
90
91
return
Redirect
::
route
(
"admin/page/index"
);
92
}
93
94
return
View
::
make
(
"admin/page/add"
,
compact
(
95
"layouts"
96
));
97
}
98
99
public
function
editAction
()
100
{
101
$files
=
$this
->
filesystem
->
listContents
(
"layouts"
);
102
$layouts
=
[];
103
104
foreach
(
$files
as
$file
)
105
{
106
$name
=
$file
[
"basename"
];
107
$layouts
[
$name
]
=
$name
;
108
}
109
110
$page
=
Input
::
get
(
"page"
);
111
$name
=
str_ireplace
(
".blade.php"
,
""
,
$page
);
112
$content
=
$this
->
filesystem
->
read
(
"pages/"
.
$page
);
113
$extracted
=
$this
->
engine
->
extractMeta
(
$content
);
114
$code
=
trim
(
$extracted
[
"template"
]);
115
$parsed
=
$this
->
engine
->
parseMeta
(
$extracted
[
"meta"
]);
116
$title
=
$parsed
[
"title"
];
117
$description
=
$parsed
[
"description"
];
118
$route
=
$parsed
[
"route"
];
119
$layout
=
$parsed
[
"layout"
];
120
121
if
(
Input
::
has
(
"save"
))
122
{
123
$validator
=
Validator
::
make
(
Input
::
all
(),
[
124
"name"
=>
"required|edit:"
.
Input
::
get
(
"page"
),
125
"route"
=>
"required"
,
126
"layout"
=>
"required"
,
127
"code"
=>
"required"
128
]);
129
130
if
(
$validator
->
fails
())
131
{
132
return
Redirect
::
route
(
"admin/page/edit"
)
133
->
withInput
()
134
->
withErrors
(
$validator
);
135
}
136
137
$meta
=
"
138
title = "
.
Input
::
get
(
"title"
)
.
"
139
description = "
.
Input
::
get
(
"description"
)
.
"
140
layout = "
.
Input
::
get
(
"layout"
)
.
"
141
route = "
.
Input
::
get
(
"route"
)
.
"
142
==
143
"
;
144
145
$name
=
"pages/"
.
Input
::
get
(
"name"
)
.
".blade.php"
;
146
147
$this
->
filesystem
->
put
(
$name
,
$meta
.
Input
::
get
(
"code"
));
148
149
return
Redirect
::
route
(
"admin/page/index"
);
150
}
151
152
return
View
::
make
(
"admin/page/edit"
,
compact
(
153
"name"
,
154
"title"
,
155
"description"
,
156
"layout"
,
157
"layouts"
,
158
"route"
,
159
"code"
160
));
161
}
162
163
public
function
deleteAction
()
164
{
165
$name
=
"pages/"
.
Input
::
get
(
"page"
);
166
$this
->
filesystem
->
delete
(
$name
);
167
168
return
Redirect
::
route
(
"admin/page/index"
);
169
}
170
}
The constructor method accepts the same injected dependencies as our layout controller did. We also define similar custom validation rules to check the names of files we want to save.
The addAction() method differs slightly in that we load the existing layout files so that we can designate the layout for each page. We also add this (and the route parameter) to the metadata saved to the page file.
The editAction() method loads the route and layout parameters (in addition to the other fields) and passes them to the edit page template, where they will be used to populate the new fields.
1
<nav
2
class=
"navbar navbar-inverse navbar-fixed-top"
3
role=
"navigation"
4
>
5
<div
class=
"container-fluid"
>
6
<div
class=
"navbar-header"
>
7
<button
type=
"button"
8
class=
"navbar-toggle"
9
data-toggle=
"collapse"
10
data-target=
"#navbar-collapse"
11
>
12
<span
class=
"sr-only"
>
Toggle navigation</span>
13
<span
class=
"icon-bar"
></span>
14
<span
class=
"icon-bar"
></span>
15
<span
class=
"icon-bar"
></span>
16
</button>
17
</div>
18
<div
class=
"collapse navbar-collapse"
id=
"navbar-collapse"
>
19
<ul
class=
"nav navbar-nav"
>
20
<li
class=
"@yield("
navigation
/
layout
/
class
")"
>
21
<a
href=
"{{ URL::route("
admin
/
layout
/
index
")
}}"
>
22
Layouts 23
</a>
24
</li>
25
<li
class=
"@yield("
navigation
/
page
/
class
")"
>
26
<a
href=
"{{ URL::route("
admin
/
page
/
index
")
}}"
>
27
Pages 28
</a>
29
</li>
30
</ul>
31
</div>
32
</div>
33
</nav>
1
<ol
class=
"breadcrumb"
>
2
<li>
3
<a
href=
"{{ URL::route("
admin
/
page
/
index
")
}}"
>
4
List Pages 5
</a>
6
</li>
7
<li>
8
<a
href=
"{{ URL::route("
admin
/
page
/
add
")
}}"
>
9
Add New Page 10
</a>
11
</li>
12
</ol>
1
@extends("admin/layout") 2
@section("navigation/page/class") 3
active 4
@stop 5
@section("content") 6
@include("admin/include/page/navigation") 7
@if (count($pages)) 8
<table
class=
"table table-striped"
>
9
<thead>
10
<tr>
11
<th
class=
"wide"
>
12
File 13
</th>
14
<th
class=
"narrow"
>
15
Actions 16
</th>
17
</tr>
18
</thead>
19
<tbody>
20
@foreach ($pages as $page) 21
@if ($page["type"] == "file") 22
<tr>
23
<td
class=
"wide"
>
24
<a
href=
"{{ $edit . $page["
basename
"]
}}"
>
25
{{ $page["basename"] }} 26
</a>
27
</td>
28
<td
class=
"narrow actions"
>
29
<a
href=
"{{ $edit . $page["
basename
"]
}}"
>
30
<i
class=
"glyphicon glyphicon-pencil"
></i>
31
</a>
32
<a
href=
"{{ $delete . $page["
basename
"]
}}"
>
33
<i
class=
"glyphicon glyphicon-trash"
></i>
34
</a>
35
</td>
36
</tr>
37
@endif 38
@endforeach 39
</tbody>
40
</table>
41
@else 42
No pages yet. 43
<a
href=
"{{ URL::route("
admin
/
page
/
add
")
}}"
>
44
create one now! 45
</a>
46
@endif 47
@stop
1
@extends("admin/layout") 2
@section("navigation/page/class") 3
active 4
@stop 5
@section("content") 6
@include("admin/include/page/navigation") 7
<form
role=
"form"
method=
"post"
>
8
<div
class=
"form-group"
>
9
<label
for=
"name"
>
Name</label>
10
<span
class=
"help-text text-danger"
>
11
{{ $errors->first("name") }} 12
</span>
13
<input
14
type=
"text"
15
class=
"form-control"
16
id=
"name"
17
name=
"name"
18
placeholder=
"new-page"
19
value=
"{{ Input::old("
name
")
}}"
20
/>
21
</div>
22
<div
class=
"form-group"
>
23
<label
for=
"route"
>
Route</label>
24
<span
class=
"help-text text-danger"
>
25
{{ $errors->first("route") }} 26
</span>
27
<input
28
type=
"text"
29
class=
"form-control"
30
id=
"route"
31
name=
"route"
32
placeholder=
"/new-page"
33
value=
"{{ Input::old("
route
")
}}"
34
/>
35
</div>
36
<div
class=
"form-group"
>
37
<label
for=
"layout"
>
Layout</label>
38
<span
class=
"help-text text-danger"
>
39
{{ $errors->first("layout") }} 40
</span>
41
{{ Form::select( 42
"layout", 43
$layouts, 44
Input::old("layout"), 45
[ 46
"id" => "layout", 47
"class" => "form-control" 48
] 49
) }} 50
</div>
51
<div
class=
"form-group"
>
52
<label
for=
"title"
>
Meta Title</label>
53
<input
54
type=
"text"
55
class=
"form-control"
56
id=
"title"
57
name=
"title"
58
value=
"{{ Input::old("
title
")
}}"
59
/>
60
</div>
61
<div
class=
"form-group"
>
62
<label
for=
"description"
>
Meta Description</label>
63
<input
64
type=
"text"
65
class=
"form-control"
66
id=
"description"
67
name=
"description"
68
value=
"{{ Input::old("
description
")
}}"
69
/>
70
</div>
71
<div
class=
"form-group"
>
72
<label
for=
"code"
>
Code</label>
73
<span
class=
"help-text text-danger"
>
74
{{ $errors->first("code") }} 75
</span>
76
<textarea
77
class=
"form-control"
78
id=
"code"
79
name=
"code"
80
rows=
"5"
81
placeholder=
"<div>Hello world</div>"
82
>
{{ Input::old("code") }}</textarea>
83
</div>
84
<input
85
type=
"submit"
86
name=
"save"
87
class=
"btn btn-default"
88
value=
"Save"
89
/>
90
</form>
91
@stop
1
@extends("admin/layout") 2
@section("navigation/page/class") 3
active 4
@stop 5
@section("content") 6
@include("admin/include/page/navigation") 7
<form
role=
"form"
method=
"post"
>
8
<div
class=
"form-group"
>
9
<label
for=
"name"
>
Name</label>
10
<span
class=
"help-text text-danger"
>
11
{{ $errors->first("name") }} 12
</span>
13
<input
14
type=
"text"
15
class=
"form-control"
16
id=
"name"
17
name=
"name"
18
placeholder=
"new-page"
19
value=
"{{ Input::old("
name
",
$
name
)
}}"
20
/>
21
</div>
22
<div
class=
"form-group"
>
23
<label
for=
"route"
>
Route</label>
24
<span
class=
"help-text text-danger"
>
25
{{ $errors->first("route") }} 26
</span>
27
<input
28
type=
"text"
29
class=
"form-control"
30
id=
"route"
31
name=
"route"
32
placeholder=
"/new-page"
33
value=
"{{ Input::old("
route
",
$
route
)
}}"
34
/>
35
</div>
36
<div
class=
"form-group"
>
37
<label
for=
"layout"
>
Layout</label>
38
<span
class=
"help-text text-danger"
>
39
{{ $errors->first("layout") }} 40
</span>
41
{{ Form::select("layout", $layouts, Input::old("layout", $layout), [ 42
"id" => "layout", 43
"class" => "form-control" 44
]) }} 45
</div>
46
<div
class=
"form-group"
>
47
<label
for=
"title"
>
Meta Title</label>
48
<input
49
type=
"text"
50
class=
"form-control"
51
id=
"title"
52
name=
"title"
53
value=
"{{ Input::old("
title
",
$
title
)
}}"
54
/>
55
</div>
56
<div
class=
"form-group"
>
57
<label
for=
"description"
>
Meta Description</label>
58
<input
59
type=
"text"
60
class=
"form-control"
61
id=
"description"
62
name=
"description"
63
value=
"{{ Input::old("
description
",
$
description
)
}}"
64
/>
65
</div>
66
<div
class=
"form-group"
>
67
<label
for=
"code"
>
Code</label>
68
<span
class=
"help-text text-danger"
>
69
{{ $errors->first("code") }} 70
</span>
71
<textarea
72
class=
"form-control"
73
id=
"code"
74
name=
"code"
75
rows=
"5"
76
placeholder=
"<div>Hello world</div>"
77
>
{{ Input::old("code", $code) }}</textarea>
78
</div>
79
<input
type=
"submit"
name=
"save"
class=
"btn btn-default"
value=
"Save"
/>
80
</form>
81
@stop
The views follow a similar pattern to those which we created for managing layout files. The exception is that we add the new layout and route fields to the add and edit page templates. We’ve used the Form::select() method to render and select the appropriate layout.
1
Route
::
any
(
"admin/page/index"
,
[
2
"as"
=>
"admin/page/index"
,
3
"uses"
=>
"PageController@indexAction"
4
]);
5
6
Route
::
any
(
"admin/page/add"
,
[
7
"as"
=>
"admin/page/add"
,
8
"uses"
=>
"PageController@addAction"
9
]);
10
11
Route
::
any
(
"admin/page/edit"
,
[
12
"as"
=>
"admin/page/edit"
,
13
"uses"
=>
"PageController@editAction"
14
]);
15
16
Route
::
any
(
"admin/page/delete"
,
[
17
"as"
=>
"admin/page/delete"
,
18
"uses"
=>
"PageController@deleteAction"
19
]);
Finally, we add the routes which will allow us to access these pages. With all this in place, we can work on displaying the website content.
Aside from our admin pages, we need to be able to catch the all requests, and route them to a single controller/action. We do this by appending the following route to the routes.php file:
1
Route
::
any
(
"{all}"
,
[
2
"as"
=>
"index/index"
,
3
"uses"
=>
"IndexController@indexAction"
4
])
->
where
(
"all"
,
".*"
);
We pass all route information to a named parameter ({all}), which will be mapped to the IndexController::indexAction() method. We also need to specify the regular expression with which the route data should be matched. With ”.*” we’re telling Laravel to match absolutely anything. This is why this route needs to come right at the end of the app/routes.php file.
1
<?
php
2
3
use
Formativ\Cms\EngineInterface
;
4
use
Formativ\Cms\FilesystemInterface
;
5
6
class
IndexController
7
extends
BaseController
8
{
9
protected
$engine
;
10
protected
$filesystem
;
11
12
public
function
__construct
(
13
EngineInterface
$engine
,
14
FilesystemInterface
$filesystem
15
)
16
{
17
$this
->
engine
=
$engine
;
18
$this
->
filesystem
=
$filesystem
;
19
}
20
21
protected
function
parseFile
(
$file
)
22
{
23
return
$this
->
parseContent
(
24
$this
->
filesystem
->
read
(
$file
[
"path"
]),
25
$file
26
);
27
}
28
29
protected
function
parseContent
(
$content
,
$file
=
null
)
30
{
31
$extracted
=
$this
->
engine
->
extractMeta
(
$content
);
32
$parsed
=
$this
->
engine
->
parseMeta
(
$extracted
[
"meta"
]);
33
34
return
compact
(
"file"
,
"content"
,
"extracted"
,
"parsed"
);
35
}
36
37
protected
function
stripExtension
(
$name
)
38
{
39
return
str_ireplace
(
".blade.php"
,
""
,
$name
);
40
}
41
42
protected
function
cleanArray
(
$array
)
43
{
44
return
array_filter
(
$array
,
function
(
$item
)
{
45
return
!
empty
(
$item
);
46
});
47
}
48
49
public
function
indexAction
(
$route
=
"/"
)
50
{
51
$pages
=
$this
->
filesystem
->
listContents
(
"pages"
);
52
53
foreach
(
$pages
as
$page
)
54
{
55
if
(
$page
[
"type"
]
==
"file"
)
56
{
57
$page
=
$this
->
parseFile
(
$page
);
58
59
if
(
$page
[
"parsed"
][
"route"
]
==
$route
)
60
{
61
$basename
=
$page
[
"file"
][
"basename"
];
62
$name
=
"pages/extracted/"
.
$basename
;
63
$layout
=
$page
[
"parsed"
][
"layout"
];
64
$layoutName
=
"layouts/extracted/"
.
$layout
;
65
$extends
=
$this
->
stripExtension
(
$layoutName
);
66
67
$template
=
"
68
@extends('"
.
$extends
.
"')
69
@section('page')
70
"
.
$page
[
"extracted"
][
"template"
]
.
"
71
@stop
72
"
;
73
74
$this
->
filesystem
->
put
(
$name
,
trim
(
$template
));
75
76
$layout
=
"layouts/"
.
$layout
;
77
78
$layout
=
$this
->
parseContent
(
79
$this
->
filesystem
->
read
(
$layout
)
80
);
81
82
$this
->
filesystem
->
put
(
83
$layoutName
,
84
$layout
[
"extracted"
][
"template"
]
85
);
86
87
$data
=
array_merge
(
88
$this
->
cleanArray
(
$layout
[
"parsed"
]),
89
$this
->
cleanArray
(
$page
[
"parsed"
])
90
);
91
92
return
View
::
make
(
93
$this
->
stripExtension
(
$name
),
94
$data
95
);
96
}
97
}
98
}
99
}
100
}
In the IndexController class, we’ve injected the same two dependencies: $filesystem and $engine. We need these to fetch and extract the template data.
We begin by fetching all the files in the app/views/pages directory. We iterate through them filtering out all the returned items which have a type of file. From these, we fetch the metadata and check if the route defined matches that which is being requested.
If there is a match, we extract the template data and save it to a new file, resembling app/views/pages/extracted/[original name]. We then fetch the layout defined in the metadata, performing a similar transformation. We do this because we still want to run the templates through Blade (so that @extends, @include, @section etc.) all still work as expected.
We filter the page metadata and the layout metadata, to omit any items which do not have values, and we pass the merged array to the view. Blade takes over and we have a rendered view!
We’ve implemented the simplest subset of October functionality. There’s a lot more going on that I would love to implement, but we’ve run out of time to do so. If you’ve found this project interesting, perhaps you would like to take a swing at implementing partials (they’re not much work if you prevent them from having metadata). Or perhaps you’re into JavaScript and want to try your hand at emulating some of the Ajax framework magic that October’s got going on…