Building a Simple Blog with Flight - Part 1
Tags: php, tutorial, blog, api
Published: 2024-07-04 05:07:59

Hey everyone! I figured it was time to showcase some of the new features that have been added to the Flight Framework for PHP. Earlier this year the original creator of Flight [Mike Cao](https://x.com/caozilla) graciously offered to transfer ownership of [mikecao/flight](https://github.com/mikecao/flight) over to a new [Flight PHP](https://github.com/flightphp) organization. Since it's been moved we've added features like [middleware](https://docs.flightphp.com/learn/middleware), [route grouping](https://docs.flightphp.com/learn/routing#route-grouping), [DIC](https://docs.flightphp.com/learn/dependency-injection-container), and other features. This post will be a little longer, but it's just because I've included a lot of code examples so you can have the right context into how your blog will get built.

First off, let's just get this out of the way. Flight is meant to be a simple framework with a few bells and whistles. It will not compete with Laravel or Symfony or Yii or Cake or [fill in the blank]. This framework is really built towards simple to medium size projects. It also caters to those who don't like "magic" in their code that's hard to understand or train to. It's geared more towards developers who are just starting to branch into frameworks instead of raw PHP with a lot of random `include` statements. 

### tl;dr

Lots of cool features, nice simple implementation, blah blah blah [here's the code](https://github.com/n0nag0n/flightphp-blog). Go to [part 2](https://dev.to/n0nag0n/building-a-simple-blog-with-flight-part-2-5acb) for the cool stuff!

## Installation

Let's use [Composer](https://getcomposer.org) to get this party started.

```
composer create-project flightphp/skeleton blog/
cd blog/
```

## Configure your New Project

First thing to do is to go to the `app/config/config.php` file where we can put any config like API keys, database credentials, and other important credentials for our app. For this blog, we'll uncomment the line with `file_path` for our SQLite database path:

```php
return [
	'database' => [
		// 'host' => 'localhost',
		// 'dbname' => 'dbname',
		// 'user' => 'user',
		// 'password' => 'password'
		'file_path' => __DIR__ . $ds . '..' . $ds . 'database.sqlite'
	],
];
```

### Create the Blog Database

Flight now comes with a command line utility called [runway](https://github.com/flightphp/runway). This allows you to create custom commands for a plugin for Flight, or even for your own project.

As part of the skeleton, it comes with a `SampleDatabaseCommand` that will give us a starting point with this blog project we are creating.

Run the below command and it should populate your database for you!

```
php runway init:sample-db
```

Next we'll open up the `app/config/services.php` file and uncomment the line for SQLite.

```php
// see how the $config variable references the config line we uncommented earlier?
$dsn = 'sqlite:' . $config['database']['file_path'];
```

Just to make sure we've got everything setup correctly, run `composer start` and then go to `http://localhost:8000/` in your browser. You should see the following screen:

![Default Home Page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mfzhhfblyy8zi1gschpy.png)

You'll also notice in the corner you have a [handy debug toolbar](https://tracy.nette.org/) with some custom Flight panels to help you understand what's going on in your application. If you hover over the various items in the toolbar, you'll see a variety of hovers that you can click on to keep sticky on the page (more on that later).

![Flight Tracy Extensions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/19uqje2an4ldwapzp6sx.png)

### Building the HTML Templates

Flight does come with a [very basic HTML templating](https://docs.flightphp.com/learn/templates) solution already in the framework. This is just fine for very simple sites or just to return a simple piece of HTML. It is recommended to use another templating platform such as Latte, Twig, or Blade. In this tutorial, we're going to use Latte because it is *awesome* and has no dependencies (you'll notice in Flight we do not like unnecessary dependencies)!

Go ahead and install [Latte](https://docs.flightphp.com/awesome-plugins/latte)

```
composer require latte/latte
```

Add this to your `services.php` 

```php
$Latte = new \Latte\Engine;
$Latte->setTempDirectory(__DIR__ . '/../cache/');

// This is fun feature of Flight. You can remap some built in functions with the framework
// to your liking. In this case, we're remapping the Flight::render() method.
$app->map('render', function(string $templatePath, array $data = [], ?string $block = null) use ($app, $Latte) {
	$templatePath = __DIR__ . '/../views/'. $templatePath;
	$Latte->render($templatePath, $data, $block);
});
```

Now that we have a templating engine in place, we can create a base HTML file. Let's create a `layout.latte` file:

```html
<!doctype html>
<html lang="en">
	<head>
        <!-- Picnic.css is a CSS framework that works out of the box with little configuration -->
		<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/picnic">
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<title>{$page_title ? $page_title.' - '}Blog Built with Flight!</title>
	</head>
	<body style="padding: 15px;">
		{block content}{/block}
	</body>
</html>
```

### Active Record Database Class

Flight has a plugin for interacting with a database called [Flight Active Record](https://docs.flightphp.com/awesome-plugins/active-record). This plugin helps you not write as much raw SQL in your apps (although sometimes it is more efficient to write a raw SQL query instead of forcing an active record/ORM/mapper to run it for you). Basically the active record extension helps you interact with rows within tables in your database: one row in a database can be mapped to an object in PHP (with autocomplete for the columns) saving time and sanity. Let's get it installed in our project.

```
composer require flightphp/active-record
```

Now you can use `runway` to create your active record classes automatically for you and it will create your properties as comments automatically (for autocomplete)! 

First let's create the posts class. The first time you run this, it needs to setup the connection for the database.

```
$ php runway make:record posts
Database configuration not found. Please provide the following details:
Driver (mysql/pgsql/sqlite): sqlite
Database file path [database.sqlite]: app/database.sqlite
Username (for no username, press enter) []: 
Password (for no password, press enter) []: 
Writing database configuration to .runway-config.json
Creating directory app/records
Active Record successfully created at app/records/PostRecord.php
```

Now we'll create the comments record class:

```
$ php runway make:record comments
```

## It's Time for your First Page!

Flight uses the MVC pattern. In order to create a new page you need to define a route in your `routes.php` file, create a new method in a controller, and then create the HTML file that the browser will serve. You can use runway to help you get started with a new controller class:

```
php runway make:controller Home
```

And you should see something similar to the following:

```
$ php runway make:controller Home
Controller successfully created at app/controllers/HomeController.php
```

If you go to `app/controllers/HomeController.php` go ahead and add this new method to your `HomeController`:

```php
/**
 * Index
 * 
 * @return void
 */
public function index(): void
{
	$this->app->render('home.latte', [ 'page_title' => 'Home' ]);
}
```

And create a new file in `app/views/home.latte` and put in this code:

```html
{extends 'layout.latte'}

{block content}
<h1>My Home Page</h1>
<p><a href="/blog">View My Blog!</a></p>
{/block}
```

Finally let's change up the routes to the `routes.php` file. Go ahead and remove any code in the routes file that begins with `$router->` and add a new route for your home router:

```php
$router->get('/', \app\controllers\HomeController::class . '->index');
```

Make sure you run `composer start` so that your development server is up. If you go to `http://localhost:8000/` in your browser, you should see something like this!

![Flight Demo Home Page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zhj51txzkkzlyk2fgwvt.png)

Now we're cookin'!

## Adding Routes for the Blog

Let's go ahead and add all the methods in your controller, routes, and html files. Let's start with adding the routes in your `routes.php` file:

```php
// Blog
$router->group('/blog', function(Router $router) {

	// Posts
	$router->get('', \app\controllers\PostController::class . '->index');
	$router->get('/create', \app\controllers\PostController::class . '->create');
	$router->post('', \app\controllers\PostController::class . '->store');
	$router->get('/@id', \app\controllers\PostController::class . '->show');
	$router->get('/@id/edit', \app\controllers\PostController::class . '->edit');
	$router->post('/@id/edit', \app\controllers\PostController::class . '->update');
	$router->get('/@id/delete', \app\controllers\PostController::class . '->destroy');
});
```

So you'll notice we use a `group()` method here to group all the routes together that start with `/blog`. We could actually rewrite the routes like the following with the `group()` method and the same thing would happen:

```php
// Posts
$router->get('/blog', \app\controllers\PostController::class . '->index');
$router->get('/blog/create', \app\controllers\PostController::class . '->create');
```

With the controller, first let's create an empty controller with `runway`:

```
php runway make:controller Post
```

You can copy the code below for your `PostController.php`:

```php
<?php

declare(strict_types=1);

namespace app\controllers;

use app\records\CommentRecord;
use app\records\PostRecord;
use flight\Engine;

class PostController
{
    /** @var Engine */
    protected Engine $app;

    /**
     * Constructor
     */
    public function __construct(Engine $app)
    {
        $this->app = $app;
    }

	/**
	 * Index
	 *
	 * @return void
	 */
	public function index(): void
	{
		$PostRecord = new PostRecord($this->app->db());
		$posts = $PostRecord->order('id DESC')->findAll();
		$CommentRecord = new CommentRecord($this->app->db());
		foreach($posts as &$post) {
			$post->comments = $CommentRecord->eq('post_id', $post->id)->findAll();
		}
		$this->app->render('posts/index.latte', [ 'page_title' => 'Blog', 'posts' => $posts]);
	}

	/**
	 * Create
	 *
	 * @return void
	 */
	public function create(): void
	{
		$this->app->render('posts/create.latte', [ 'page_title' => 'Create Post']);
	}

	/**
	 * Store
	 *
	 * @return void
	 */
	public function store(): void
	{
		$postData = $this->app->request()->data;
		$PostRecord = new PostRecord($this->app->db());
		$PostRecord->title = $postData->title;
		$PostRecord->content = $postData->content;
		$PostRecord->username = $postData->username;
		$PostRecord->created_at = gmdate('Y-m-d H:i:s');
		$PostRecord->updated_at = null;
		$PostRecord->save();
		$this->app->redirect('/blog');
	}

	/**
	 * Show
	 *
	 * @param int $id The ID of the post
	 * @return void
	 */
	public function show(int $id): void
	{
		$PostRecord = new PostRecord($this->app->db());
		$post = $PostRecord->find($id);
		$CommentRecord = new CommentRecord($this->app->db());
		$post->comments = $CommentRecord->eq('post_id', $post->id)->findAll();
		$this->app->render('posts/show.latte', [ 'page_title' => $post->title, 'post' => $post]);
	}

	/**
	 * Edit
	 *
	 * @param int $id The ID of the post
	 * @return void
	 */
	public function edit(int $id): void
	{
		$PostRecord = new PostRecord($this->app->db());
		$post = $PostRecord->find($id);
		$this->app->render('posts/edit.latte', [ 'page_title' => 'Update Post', 'post' => $post]);
	}

	/**
	 * Update
	 *
	 * @param int $id The ID of the post
	 * @return void
	 */
	public function update(int $id): void
	{
		$postData = $this->app->request()->data;
		$PostRecord = new PostRecord($this->app->db());
		$PostRecord->find($id);
		$PostRecord->title = $postData->title;
		$PostRecord->content = $postData->content;
		$PostRecord->username = $postData->username;
		$PostRecord->updated_at = gmdate('Y-m-d H:i:s');
		$PostRecord->save();
		$this->app->redirect('/blog');
	}

	/**
	 * Destroy
	 *
	 * @param int $id The ID of the post
	 * @return void
	 */
	public function destroy(int $id): void
	{
		$PostRecord = new PostRecord($this->app->db());
		$post = $PostRecord->find($id);
		$post->delete();
		$this->app->redirect('/blog');
	}
}
```

Let's kill some time and talk about a few things that are going on in the controller.

First off we are now using our new active record classes:

```php
$PostRecord = new PostRecord($this->app->db());
$posts = $PostRecord->order('id DESC')->findAll();
```

We are injecting the database we setup in the `services.php` file above with `$this->app->db();`. Technically we could also just use `Flight::db()` as this points to the global `$app` variable. 

Active Record classes are really helpful to simplify interactions with a database. We could rewrite the above in the following code:

```php
$posts = $this->app->db()->fetchAll("SELECT * FROM posts ORDER BY id DESC");
```

This might not be the best example of how helpful an active record could be. But [in part 2](https://dev.to/n0nag0n/building-a-simple-blog-with-flight-part-2-22ol) I'll show you some hidden gems inside these classes that make it so much better than writing raw SQL.

Now let's talk HTML files. Here are the files we'll need for the post routes:

`app/views/posts/index.latte`

```html
{extends '../layout.latte'}

{block content}

<h1>My Amazing Blog</h1>
<p>Welcome to my blog!</p>
<p><a class="button" href="/blog/create">Create a new post</a></p>
{foreach $posts as $post}
	{first}
	<h2>Recent Posts</h2>
	{/first}
	<hr>
	<h3><a href="/blog/{$post->id}">{$post->title}</a></h3>
	<p><small>By: {$post->username} on {$post->created_at|date:'d.m.Y G:i a'}</small></p>
	<p>Comments: {count($post->comments)}
	<p>{$post->content|truncate:100}</p>
	<hr>
	<a class="pseudo button" href="/blog/{$post->id}/edit">Update</a> - <a class="pseudo button" href="/blog/{$post->id}/delete">Delete</a>
{/foreach}

{/block}
```

`app/views/posts/show.latte`

```html
{extends '../layout.latte'}

{block content}
<a href="/blog">&lt; Back to blog</a>
<h1>{$post->title}</h1>
<p>Created by: {$post->username} on {$post->created_at|date:'d.m.Y G:i a'}.</p>
<div>
	{$post->content|breakLines}
</div>
<p n:if="$post->update_at">Last update: {$post->update_at|date:'d.m.Y G:i a'}.</p>

<h2>Comments</h2>
{foreach $post->comments as $comment}
<div>
	<p>{$comment->username} on {$comment->created_at|date:'d.m.Y G:i a'}.</p>
	<div>
		{$comment->content|breakLines}
	</div>
	<hr>
	<a class="pseudo button" href="/blog/{$post->id}/comment/{$comment->id}/delete">Delete</a>
</div>
{else}
<p>No comments yet.</p>
{/foreach}

<h2>Add comment</h2>
<form action="/blog/{$post->id}/comment" method="post">
	<div>
		<label for="username">Username:</label>
		<input name="username" id="username" placeholder="Username" required />
	</div>
	<div>
		<label for="content">Comment:</label>
		<textarea name="content" id="content" placeholder="Comment" required></textarea>
	</div>
	<div>
		<button type="submit">Add Comment</button>
	</div>
</form>
{/block}
```

`app/views/posts/create.latte`

```html
{extends '../layout.latte'}

{block content}
<h1>Create a Post</h1>
<form action="/blog" method="post">
	<label><input type="text" name="title" placeholder="Title" required></label>
	<label><textarea name="content" placeholder="Content" required></textarea></label>
	<label><input type="text" name="username" placeholder="Username" required></label>

	<button type="submit">Create</button>
</form>
{/block}
```

`app/views/posts/edit.latte`

```html
{extends '../layout.latte'}

{block content}
<h1>Update a Post</h1>
<form action="/blog/{$post->id}/edit" method="post">
	<label for="title">Title</label>
	<input type="text" name="title" placeholder="Title" value="{$post->title}" required>

	<label for="content">Content</label>
	<label><textarea name="content" placeholder="Content" required>{$post->content}</textarea>

	<label for="username">Username</label>
	<label><input type="text" name="username" placeholder="Username" value="{$post->username}" required>

	<button type="submit">Update</button>
</form>
{/block}
```

## Create a new post

Now that we've got all the pieces in place, you should be able to load up your blog page, create a new post, see a post, and delete a post. You may have noticed we've included a comment form but the form doesn't actually work. We can fix that real quick! Let's create a controller with `runway`:

```
php runway make:controller Comment
```

Now you can make the `CommentController.php` look like the following:

```php
<?php

declare(strict_types=1);

namespace app\controllers;

use app\records\CommentRecord;
use flight\Engine;

class CommentController
{
    /** @var Engine */
    protected Engine $app;

    /**
     * Constructor
     */
    public function __construct(Engine $app)
    {
        $this->app = $app;
    }

	/**
	 * Store
	 * 
	 * @param int $id The post ID
	 * 
	 * @return void
	 */
	public function store(int $id): void
	{
		$postData = $this->app->request()->data;
		$CommentRecord = new CommentRecord($this->app->db());
		$CommentRecord->post_id = $id;
		$CommentRecord->username = $postData->username;
		$CommentRecord->content = $postData->content;
		$CommentRecord->created_at = gmdate('Y-m-d H:i:s');
		$CommentRecord->updated_at = null;
		$CommentRecord->save();
		$this->app->redirect('/blog/' . $id);
	}

	/**
	 * Destroy
	 * 
	 * @param int $id The post ID
	 * @param int $comment_id The comment ID
	 * 
	 * @return void
	 */
	public function destroy(int $id, int $comment_id): void
	{
		$CommentRecord = new CommentRecord($this->app->db());
		$CommentRecord->find($comment_id);
		$CommentRecord->delete();
		$this->app->redirect('/blog/' . $id);
	}

}
```

Now let's add a couple other routes in the group chunk of code in `routes.php`

```php
// Blog
$router->group('/blog', function(Router $router) {

	// Posts

	// post routes...

	// Comments
	$router->post('/@id/comment', \app\controllers\CommentController::class . '->store');
	$router->get('/@id/comment/@comment_id/delete', \app\controllers\CommentController::class . '->destroy');
});
```

## Conclusion (sort of)

With these two additions to the code, you have a fully functioning blog built with Flight! This got the job done and you now have a blog, but the code is somewhat clunky and could be improved to have some pretty nifty features like middleware, permissions, and writing less code! [Hop over to part 2](https://dev.to/n0nag0n/building-a-simple-blog-with-flight-part-2-5acb)

Go ahead and leave any questions in comments below or [join us in the chatroom](https://matrix.to/#/#flight-php-framework:matrix.org)!

> If you want to see the final product with all the improvements [here's the code](https://github.com/n0nag0n/flightphp-blog)!