4.2. Writing a Package
The framework of Cradle is all about packages and there are three types of packages (but they basically can do the same thing, just located in different places).
- Root packages - are found in your project root directory. All applications as in
/app/admin
,/app/www
and modules as in/module/utility
are all kinds of root packages. - Vendor packages - are found in the
/vendor/
folder of your project directory. These packages are from third party vendors./vendor/cradlephp/cradle-system
,/vendor/cradlephp/cradle-profile
,/vendor/cradlephp/cradle-auth
are all kinds of vendor packages. - Pseudo packages - are located in memory. It does not have any files or folders.
The
global
package is an example of a pseudo package.
4.2.1. Setting up the Package
This chapter continues from the last chapter,
4.1. Developing the Front End and in
this chapter we will create an Article Package from the ground up. Create a
file called /module/article/.cradle.php
, where the /module
should already
exist and add the following PHP code. This will be called the Article Bootstrap.
Figure 4.2.1.A. Setting up the bootstrap
require_once __DIR__ . '/src/events.php';
WARNING: Don't forget the `<?php` tag
Next, create the Article Events at /module/article/src/events.php
and add
the following PHP code.
Figure 4.2.1.B. Writing an Event
use Cradle\Storm\SqlFactory;
$this->on('article-detail', function($req, $res) {
//get the article ID
$articleId = $req->getStage('article_id');
//if it is not a number
if (!is_numeric($articleId)) {
//set an error and return
return $res->setError(true, 'Invalid ID');
}
//get the PDO object from services
$pdo = $this->package('global')->service('sql-main');
//load up Storm ORM
$database = SqlFactory::load($pdo);
//get the article
$article = $database
->search('article')
->innerJoinUsing('article_profile', 'article_id')
->innerJoinUsing('profile', 'profile_id')
->filterByArticleId($articleId)
->getRow();
//if no article found
if (!$article) {
//set an error and return
return $res->setError(true, 'Not Found');
}
//set the results
$res->setResults($article);
});
The goal of this event article-detail
is to produce the information of an
Article Object, given its article_id
. If the ID is invalid or the database
can’t find it, the event should set an error. There are a few concepts in the
above example that need to be explained.
First if there is no article_id
, we can set an error via
$res->setError(true, 'Invalid ID')
. Since we should not do anything on an
invalid ID we should simply return.
We can access a service in our config via $this->package('global')->service('sql-main');
,
which we cover in 3.1. Services & Settings.
In this case sql-main
returns a PDO object. We pass this PDO object to
SqlFactory::load($pdo)
to load up the Storm ORM, and then from here get
the article from the database.
Before we can test this event, we need to register this package in the
configuration. Open /config/packages.php
and add the Article Package to
the bottom of the list just like the following example.
Figure 4.2.1.C. Registering the Package
return array (
...
'/module/article' =>
array (
'active' => true,
),
);
To test this event in terminal, go to the root directory of your project and execute the following bash command.
Figure 4.2.1.D. Testing the Package
$ bin/cradle article-detail article_id=1
{"results":{"profile_id":"1","article_id":"1","article_title":"What is the Fate of the Furious?","article_detail":"I don't understand if <b>Jason Stathom<\/b> killed <b>Sung Kang<\/b>, <i>(<b>Vin Desiel's<\/b> asian friend). <\/i>How could they be cool with each other in future \"<b>Fast and Furious\"<\/b> films?","article_status":"published","article_published":"2019-01-30 14:00:00","article_active":"1","article_created":"2019-01-25 13:27:18","article_updated":"2019-01-28 07:11:57","article_references":"{\"reference_link\": \"https:\/\/ew.com\/movies\/2017\/04\/15\/fate-furious-han-shaw-chris-morgan\/\", \"reference_quote\": \"Statham actually joined the franchise at the end of Fast & Furious 6, when it was revealed that he killed longtime Toretto crew member Han (Sung Kang) as the first step of his vengeance mission.\", \"reference_title\": \"The Fate of the Furious: Screenwriter Chris Morgan talks Shaw... and Han\", \"reference_publication\": \"Entertainment Weekly\"}","profile_name":"John Doe","profile_active":"1","profile_created":"2019-01-20 06:43:42","profile_updated":"2019-01-20 06:43:42"}}
The next thing we probably want to do is add the Article Object’s comments
to the result set. Lets go back to the Article Events
(/module/article/src/events.php
) and add another SQL query like the
following code snippet.
Figure 4.2.1.E. Adding Comments
...
$this->on('article-detail', function($req, $res) {
//get the article ID
$articleId = $req->getStage('article_id');
...
//get all the article comments
$comments = $database
->search('comment')
->innerJoinUsing('comment_profile', 'comment_id')
->innerJoinUsing('profile', 'profile_id')
->innerJoinUsing('article_comment', 'comment_id')
->filterByArticleId($articleId)
->getRows();
//if there are comments
if (!empty($comments)) {
//add the comments
$article['comment'] = $comments;
}
//set the results
$res->setResults($article);
});
If you run the command line test again bin/cradle article-detail article_id=1
,
you will see the comments have been added. The above example is kind of how the
event system-model-detail
works and we could just call system-model-detail
instead of writing this.
While the system can deal with basic relations, the limitations is that it only
cases for one level deep. What that means is, the system can auto-determine
article->comment
and comment->comment
but it doesn’t case for
article->comment->comment
. In the case of our example, it would make sense to
write a custom event like article-detail
to achieve the article->comment->comment
result set.
In the Article Events (/module/article/src/events.php
) let’s change the code
inside of if (!empty($comments))
with the following code.
Figure 4.2.1.F. Adding More Comments
...
$this->on('article-detail', function($req, $res) {
...
//if there are comments
if (!empty($comments)) {
$commentIds = [];
$commentsWithChildren = [];
//loop through the comments
foreach($comments as $comment) {
$commentId = $comment['comment_id'];
//add to the ID set
$commentIds[] = $commentId;
//organize the comments by ID
$commentsWithChildren[$commentId] = $comment;
}
//set up the SQL IN filter
$filter = sprintf(
'comment_id_1 IN (%s)',
implode(',', $commentIds)
);
//get all the sub comments in 1 query
$subcomments = $database
->search('comment')
->innerJoinOn('comment_comment', 'comment_id_2=comment_id')
->innerJoinUsing('comment_profile', 'comment_id')
->innerJoinUsing('profile', 'profile_id')
->addFilter($filter)
->getRows();
//loop through each sub comment
foreach ($subcomments as $comment) {
$primaryCommentId = $comment['comment_id_1'];
$secondaryCommentId = $comment['comment_id_2'];
//attach it to the existing comments as children
$primaryComment = $commentsWithChildren[$primaryCommentId];
$primaryComment['children'][$secondaryCommentId] = $comment;
$commentsWithChildren[$primaryCommentId] = $primaryComment;
}
//add the comments
$article['comment'] = $commentsWithChildren;
}
//set the results
$res->setResults($article);
});
Our article-detail
event in all of its glory should look like the following code.
$this->on('article-detail', function($req, $res) {
$articleId = $req->getStage('article_id');
if (!is_numeric($articleId)) {
return $res->setError(true, 'Invalid ID');
}
$pdo = $this->package('global')->service('sql-main');
$database = SqlFactory::load($pdo);
$article = $database
->search('article')
->innerJoinUsing('article_profile', 'article_id')
->innerJoinUsing('profile', 'profile_id')
->filterByArticleId($articleId)
->getRow();
if (!$article) {
return $res->setError(true, 'Not Found');
}
$comments = $database
->search('comment')
->innerJoinUsing('comment_profile', 'comment_id')
->innerJoinUsing('profile', 'profile_id')
->innerJoinUsing('article_comment', 'comment_id')
->filterByArticleId($articleId)
->getRows();
if (empty($comments)) {
return $res->setResults($article);
}
$commentIds = [];
$commentsWithChildren = [];
foreach($comments as $comment) {
$commentId = $comment['comment_id'];
$commentIds[] = $commentId;
$commentsWithChildren[$commentId] = $comment;
}
$filter = sprintf(
'comment_id_1 IN (%s)',
implode(',', $commentIds)
);
$subcomments = $database
->search('comment')
->innerJoinOn('comment_comment', 'comment_id_2=comment_id')
->innerJoinUsing('comment_profile', 'comment_id')
->innerJoinUsing('profile', 'profile_id')
->addFilter($filter)
->getRows();
foreach ($subcomments as $comment) {
$primaryCommentId = $comment['comment_id_1'];
$secondaryCommentId = $comment['comment_id_2'];
$primaryComment = $commentsWithChildren[$primaryCommentId];
$primaryComment['children'][$secondaryCommentId] = $comment;
$commentsWithChildren[$primaryCommentId] = $primaryComment;
}
$article['comment'] = $commentsWithChildren;
$res->setResults($article);
});
While this would probably work in all cases, as programmers we should also be thinking about abstraction. In the Article Package we are working on, let’s add a class called SqlService to abstract out the SQL calls.
4.2.2. Adding a Class
Before we can do that, we need to tell Composer about the new
Article Name Space by registering it in /composer.json
like the following
json code.
Figure 4.2.2.A. composer.json
{
...
"autoload": {
"psr-4": {
"Cradle\\Module\\Utility\\": "module/utility/src/",
"Cradle\\Module\\Article\\": "module/article/src/"
}
},
...
}
Next in terminal, we need to issue the following Composer command to rebuild its list of name space locations.
Figure 4.2.2.B. Dump Autoload
$ composer dump-autoload
Let’s next create the SqlService at /module/article/src/SqlService.php
and
paste the following code.
Figure 4.2.2.B SqlService
namespace Cradle\Module\Article;
use Cradle\Storm\SqlFactory;
use PDO as SqlResource;
class SqlService
{
protected $resource;
public function __construct(SqlResource $resource)
{
//load up Storm ORM
$this->resource = SqlFactory::load($resource);
}
public function addChildrenComments(array $comments): array
{
//if no comments
if (empty($comments)) {
//return the comments as is
return $comments;
}
$commentIds = [];
$commentsWithChildren = [];
//loop through the comments
foreach($comments as $comment) {
$commentId = $comment['comment_id'];
//add to the ID set
$commentIds[] = $commentId;
//organize the comments by ID
$commentsWithChildren[$commentId] = $comment;
}
//set up the SQL IN filter
$filter = sprintf(
'comment_id_1 IN (%s)',
implode(',', $commentIds)
);
//get all the sub comments in 1 query
$subcomments = $this->resource
->search('comment')
->innerJoinOn('comment_comment', 'comment_id_2=comment_id')
->innerJoinUsing('comment_profile', 'comment_id')
->innerJoinUsing('profile', 'profile_id')
->addFilter($filter)
->getRows();
//loop through each sub comment
foreach ($subcomments as $comment) {
$primaryCommentId = $comment['comment_id_1'];
$secondaryCommentId = $comment['comment_id_2'];
//attach it to the existing comments as children
$primaryComment = $commentsWithChildren[$primaryCommentId];
$primaryComment['children'][$secondaryCommentId] = $comment;
$commentsWithChildren[$primaryCommentId] = $primaryComment;
}
//return the new array
return $commentsWithChildren;
}
}
So first, we created a namespace called Cradle\Module\Article
in both the
/composer.json
and in /module/article/src/SqlService.php
. We then copied the
namespace use Cradle\Storm\SqlFactory
from the Article Events
(/module/article/src/events.php
) as well as added an alias for PDO
described via use PDO as SqlResource
. We did this in order to require the
PDO object in our construct that looks like
public function __construct(SqlResource $resource)
. Then we created a public
method called addChildrenComments()
where the body of that method, we copied from
the Article Events.
We can now abstract parts out from the article-detail
like the following code.
Figure 4.2.2.B Event Abstraction
use Cradle\Module\Article\SqlService;
$this->on('article-detail', function($req, $res) {
//call the model detail event
$req->setStage('schema', 'article');
$this->trigger('system-model-detail', $req, $res);
//if there is an error
if ($res->isError()) {
//there's nothing else to do
return;
}
//get the articles
$article = $res->getResults();
//load up the SqlService Class
$pdo = $this->package('global')->service('sql-main');
$database = new SqlService($pdo);
//add the comment children
$article['comment'] = $database->addChildrenComments($article['comment']);
//set the results
$res->setResults($article);
});
As you noticed, we re-used system-model-detail
instead of our original code to
get the Article Object and Comment Objects. We finally called our new
SqlService->addChildrenComments()
to add the additional comment->comment
result set. Let’s test this one more time on our command line to make sure
everything still works.
Figure 4.2.2.C. Testing the Package
$ bin/cradle article-detail article_id=1
So now that we have the capabilities to retrieve an article->comment->comment
result set, let’s use our new Article Package in the front end.
Go to the Article Controller (/app/www/src/controller/article.php
) and
replace the code calling system-model-detail
with our new article-detail
instead. The following shows how it should have been done.
4.2.3. Connecting with the Controller
Figure 4.2.3.A. Article Controller
$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();
}
//get the article
$this->trigger('article-detail', $req, $res);
//if there is no data
if (!$res->hasResults()) {
//let the 404 catch this
return;
}
...
});
Lastly, let’s go in to the Article Detail Template
(/app/www/src/template/article/detail.html
) and add the following
{{#if children}}
case.
Figure 4.2.3.B. Article Detail Template
<div class="container">
...
<h3 class="mt-5">Comments</h3>
{{#each item.comment}}
...
{{#if children}}
{{#each children}}
<div class="ml-5 mt-3">
<p>{{profile_name}} {{relative comment_created}}:</p>
{{{comment_detail}}}
</div>
{{/each}}
{{/if}}
{{/each}}
...
</div>
4.2.4. Conclusion
So we created a package from the ground up and linked it with a controller. A package can be accessed programmaticall, by browser, or command line. In the next chapter we will be covering the Storm ORM in 4.3 Intro to Storm.