4.1. Developing the Front End
This chapter continues from the 2. Working the Admin section and relies that you have properly set up the Article Schema. If you need a refresher, make sure you performed the steps needed from the following chapters.
So we have an admin great! What we are going to learn in this chapter is how to
programmatically access schemas and models from the code, specifically the front
end. The system has a built in front end package called /app/www
, in which we
will be primarily working with.
4.1.1. Article Search
The goal of this section is to create a working Article Search Page.
Open /app/www/.cradle.php
and include a new controller called article.php
like the following code snippet.
Figure 4.1.1.A. Including the Article Controller
/**
* This file is part of a Custom Project.
*/
include_once __DIR__ . '/src/controller/static.php';
include_once __DIR__ . '/src/controller/article.php';
include_once __DIR__ . '/src/events.php';
...
The .cradle.php
file is primarily used for is including the needed files from
a package’s file system (which is arbitrary). Create the Article Controller
at /app/www/src/controller/article.php
and add the following
Article Search Route.
Figure 4.1.1.B. Adding the Article Search Route
$this->get('/articles', function($req, $res) {
//get all the articles
$req->setStage('schema', 'article');
$this->trigger('system-model-search', $req, $res);
//set the template variables
$data = $res->getResults();
//render the body
$body = $this->package('/app/www')->template('article/search', $data);
//Set the page content
$res
->setPage('title', 'Articles')
->setContent($body);
//Render a page
$this->trigger('www-render-page', $req, $res);
});
The purpose of the Article Search Route route is to render a page with a list of Article Objects. It does that by first getting the Article Objects from the database, then binding the Article Objects data to a body template, then finally passing the body to the page to be rendered. There are a few of concepts in the above example that need to be explained.
First, $this->trigger('system-model-search', $req, $res)
calls a database
search where all you need to do is pass a schema name, in this case article
.
You can find more about system-model-search
in
4.A. Reference: Events.
The request object has a key called stage
which holds all the data needed to
process a response. This stage
data usually has $_GET
and $_POST
automatically populated as well as route variables. In this case we are
manually adding schema=article
via $req->setStage('schema', 'article')
.
Every package can have methods defined on the fly (or virtual methods). Package
methods can be accessed like $this->package('/app/www')
where /app/www
is
the name of the package. In the code snippet above, we are calling a package
method called template()
. The definition of this method can be found at
/app/www/src/bootstrap/template.php
.
Finally, a package can have events and in the case of the code snippet above it
calls an event named www-render-page
which will effectively render the rest of
the page given the body. The definition of this event can be found at
/app/www/src/events.php
.
The next thing to do is create an Article Search Template representing the
body of this Article Search Page. Create the Article Search Template at
/app/www/src/template/article/search.html
and paste the following HTML.
Figure 4.1.1.C. Making the Article Search Template
<div class="container">
<h1>Latest Articles</h1>
{{#each rows}}
<hr />
<h3>{{article_title}}</h3>
<p>{{relative article_published}}</p>
<a href="/article/">Read More</a>
{{/each}}
</div>
If you noticed, this template file uses a templating engine called Handlebars. Handlebars was chosen for this package because of its availability to use on other programming languages and even on the client side via JavaScript.
In Figure 4.1.1.B. we passed in a variable called $data
to the body
template. This data becomes accessible to the template engine using template
variables like {{article_title}}
. In other words, this
means that there is a variable called article_title
also accessible by
$data['article_title']
.
On your browser, visit http://127.0.0.1:8888/articles
to see the
Article Search Page.
Figure 4.1.1.D. Article Search Page
If you have a few articles you will notice that the order is starting by the
first one created (rather than latest first). We can easily change the order
to what is expected by going back to the Article Controller
(/app/www/src/controller/article.php
) and adding a sort on stage
like the
following code snippet.
Figure 4.1.1.E. Ordering Articles
$this->get('/articles', function($req, $res) {
//get all the articles
$req
->setStage('schema', 'article')
->setStage('sort', 'article_published', 'DESC');
$this->trigger('system-model-search', $req, $res);
...
});
If you view the Article Search Page once again you will see the order has changed. The template also provided a Read More link when clicked goes to a 404 error page. Our next step is to create that detail page.
4.1.2. Article Detail
Let’s go back to the Article Controller (/app/www/src/controller/article.php
)
and add the following Article Detail Route.
Figure 4.1.2.A. Adding the Article Detail Route
...
$this->get('/article/:article_id', function($req, $res) {
$data = [];
//get the article
$req->setStage('schema', 'article');
$this->trigger('system-model-detail', $req, $res);
//if there is no data
if (!$res->hasResults()) {
//let the 404 catch this
return;
}
//add the article detail
$data['item'] = $res->getResults();
//render the body
$body = $this->package('/app/www')->template('article/detail', $data);
//Set Content
$res
->setPage('title', $data['item']['article_title'])
->setContent($body);
//Render a page
$this->trigger('www-render-page', $req, $res);
});
While this looks very similar to the article search route, let’s additionally
cover the difference. The first is the route path itself defined as
/article/:article_id
having the :article_id
route variable. What ever value
the user enters for this will be passed to our request stage
data.
Last, $this->trigger('system-model-detail', $req, $res)
calls a database
search where all you need to do is pass a schema name, in this case article
.
Since the article_id
is already in stage, there is no need to set it again.
You can find more about system-model-detail
in
4.A. Reference: Events.
The next thing to do is create an Article Detail Template representing the
body of this page. Create the Article Detail Template at
/app/www/src/template/article/detail.html
and paste the following HTML.
Figure 4.1.2.B. Making the Article Detail Template
<div class="container">
<h1>{{item.article_title}}</h1>
<p>{{relative item.article_published}}</p>
{{{item.article_detail}}}
<h3 class="mt-5">Comments</h3>
{{#each item.comment}}
<hr />
<p>{{profile_name}} {{relative comment_created}}:</p>
{{{comment_detail}}}
{{/each}}
</div>
On your browser, visit http://127.0.0.1:8888/article/1
to see the
Article Detail Page.
Figure 4.1.2.C. Article Detail Page
While this is great, a lot of pages these days have some sort of user interaction where the base is a form submission. In the next section we will be creating the ability for a user to comment on an Article Object.
4.1.3. Adding a Comment
In the same Article Detail Template (/app/www/src/template/article/detail.html
),
under the Comments section, create a form that looks like the code below.
Figure 4.1.3.A. Adding a Comment Form
<div class="container">
...
<h3 class="mt-5">Comments</h3>
{{#each item.comment}}
<hr />
<p>{{profile_name}} {{relative comment_created}}:</p>
{{{comment_detail}}}
{{/each}}
<h4 class="mt-5">Add a Comment</h4>
<form method="post">
<div class="form-group{{#if errors.comment_detail}} has-error{{/if}}">
<textarea class="form-control" name="comment_detail"></textarea>
{{#if errors.comment_detail}}
<div class="help-text">{{errors.comment_detail}}</div>
{{/if}}
</div>
<button class="btn btn-primary">Submit</button>
</form>
</div>
In this form it’s important to notice a few things. First is that the method of
this form is set to post via method="post"
. The next thing is that we did not
specify an action path, so by default will use this same page as the action path.
Once we have our template updated, our next step is to process this form. Go back
to the Article Controller (/app/www/src/controller/article.php
) and add the
following POST Article Detail Route that will process the form submission.
Figure 4.1.3.B. Processing a Comment
...
$this->post('/article/:article_id', function($req, $res) {
//get the article id
$articleId = $req->getStage('article_id');
//setup the routing path
$route = sprintf('/article/%s', $articleId);
//get the session profile id
$profileId = $req->getSession('me', 'profile_id');
if (!$profileId) {
$res->setError(true, 'Must be logged in to comment.');
// go back to GET /article/:article_id route
return $this->routeTo('get', $route, $req, $res);
}
//create the comment
$req
->setStage('schema', 'comment')
->setStage('profile_id', $profileId);
$this->trigger('system-model-create', $req, $res);
//if there was an error creating the comment
if ($res->isError()) {
// go back to GET /article/:article_id route
return $this->routeTo('get', $route, $req, $res);
}
//get the comment id
$commentId = $res->getResults('comment_id');
//link the article to the comment
$req
->setStage('schema1', 'article')
->setStage('schema2', 'comment')
->setStage('comment_id', $commentId);
$this->trigger('system-relation-link', $req, $res);
//if there was an error linking the article to the comment
if ($res->isError()) {
// go back to GET /article/:article_id route
return $this->routeTo('get', $route, $req, $res);
}
//it was good
//get the global package
$global = $this->package('global');
//add a happy message
$global->flash('Comment Added', 'success');
//redirect to /article/:article_id
$global->redirect($route);
});
The purpose of the POST Article Detail Route is to process the new Comment Object to be created in the database and link the Comment Object to the current Article Object. If there is an error at any time, the POST Article Detail Route should re-route to the Article Detail Route where the error will be outputted. If it was successful, it should notify the user that it was successful and redirect to the Article Detail Route. There are a few of concepts in the above example that need to be explained.
First, $req->getSession('me');
accesses the current session data
(the information of whom ever is logged in). In this case we just need the
profile_id
so we fast forward and call $req->getSession('me', 'profile_id')
instead. Since all logged in users are assigned a profile, not having a
profile_id
would mean that this user is actually logged out.
In this Comment Schema, we created a one-to-one (1:1) relationship with profile. This
means we need to provide a profile_id
when we create a comment. So this is why
we set the requirement $res->setError(true, 'Must be logged in to comment.');
.
Next, we create the comment by using $this->trigger('system-model-create', $req, $res);
.
This first filters and validates the data provided before creating the
Comment Object or provides an error message if not. You can find more about
system-model-create
in 4.A. Reference: Events.
INFORMATION system-model-create and system-model-update handle the validation as
defined by your article schema.
After the comment is created, the next thing to do is link the Comment Object
to the Article Object. We can do this by $this->trigger('system-relation-link', $req, $res);
,
provided that schema1=article
, schema2=comment
, and both the article_id
and
comment_id
are set in stage
. You can find more about system-relation-link
in
4.A. Reference: Events.
The last thing we need to do since no matter what, the POST Article Detail Route will eventually go to Article Detail Route, we need to case for errors in the POST Article Detail Route.
Let’s go back to the Article Controller (/app/www/src/controller/article.php
),
find the POST Article Detail Route and add the following code snippet inside.
Figure 4.1.3.C. Casing for Form Results
...
$this->get('/article/:article_id', function($req, $res) {
$data = [];
//if this is a return back from processing the comment form
if ($req->hasPost()) {
$data['form'] = $res->getPost();
}
//and it's has of an error
if ($res->isError()) {
//pass the error messages to the template
$res->setFlash($res->getMessage(), 'error');
$data['errors'] = $res->getValidation();
}
...
});
On your browser, visit http://127.0.0.1:8888/article/detail/1
to see your full
page with form submission working.
4.1.4. Conclusion
We worked with articles in the front end using the /app/www
package. In our
next chapter 4.2. Writing a Package,
we will work through creating a package from scratch.