How To Code on PHP7 Without using a Framework

Written by dhgouveia | Published 2020/09/28
Tech Story Tags: php | framework | backend | mvc | php7 | web-development | programming | composer | web-monetization

TLDR How To Code on PHP7 Without using a Framework How To Write a small API project without using a framework. The goal is to create a simple Blog Api, each post will have an id, title, and body, you will able to list, create and view a post, we won’t use any database, a simple JSON file that will act as DB should be enough, all request/responses will be in JSON format. The code is available in Gitlab.com/dhgouveia/medium-blog-api.via the TL;DR App

Since the introduction of Composer package manager and the PHP standards, writing PHP became easier and more manageable, whereas in the past you were almost forced to use a framework to maintain your project in a professional matter, nowadays this is not necessary, and today I will show you how to glue together a small API project with basic routing, third party packages, and testing without a framework.
There are few reasons why you don’t want to use a framework, you are creating a library, a small app/API, have more control, and so forth, depending on your cases you might want to use a framework don’t get me wrong.
Our goal is to create a simple Blog Api, each post will have an id, title, and body, you will able to list, create and view a post, we won’t use any database, a simple JSON file that will act as DB should be enough, all request/responses will be in JSON format
As you see, there are some fields and features missing, like a slug, summary, published date, author, tags, categories, and so forth, or the ability to delete/update, I decided to not implement those, and I’ll briefly explain some classes and code without getting into too much detail to make this article shorter, if you need an extra explanation of any step please leave it in the comments and I will do my best to help you there.

Prerequisites

  • PHP 7.0 or greater
  • Composer
  • Basic knowledge of PHP, classes, composer, MVC pattern
Ok, let’s start!

Setting up Composer

The first thing we need to do is create our 
composer.json
 needed to add 3rd party packages and manage our project with the autoloading feature, this will make importing classes easier.
Create a folder and type 
composer init
 in your terminal and fill the information, it will create the 
composer.json
 file for us, then create our basic folder structure with some empty files called 
index.php
,
config.php
and an empty folder called 
App
Let’s add the first package by using the command line 
composer require monolog/monolog:1.25.1
 , it creates a 
vendor
 folder with the package we just added and a file called 
autoload.php
 , this file will contain all the path to the classes we add from 3rd parties and ours, 
monolog
 is a package to create logs files that will be used later on
Open 
index.php
 and fill it with:
<?php
require __DIR__ . '/vendor/autoload.php';
modify the 
composer.json
 by adding the autoload entry after the type entry
"type": "project",
"autoload": {
  "psr-4": {
    "App\\": "App/"
   }
},
then type 
composer dump-autoload
 to update the autoload entries, the
autoload
 entry will register all our classes to be used anywhere in our app,
psr-4
 is a more flexible autoloading standard specification than
psr-0
 , you don’t need to regenerate the autoloader when you add classes for example.
By now, the app is already setup to work with composer, you can run 
php index.php
 in the terminal, if no error is shown it means is working, this shouldn't output anything

Adding our first class

Let’s make a Config helper to use across the project, we are going to have 2 files,
config.php
 at the root of the project, with some settings for the app, here is where you put your API Key, Cache setting, etc, and you should have a different one base on your environment (test, stage, prod), and the other file will be 
App/Lib/Config.php
 to read those variables
Open 
config.php
 and fill it with:
<?php
return [
 'LOG_PATH' => __DIR__ . './logs',
];
create a new file inside 
App/Lib/
 called it 
Config.php
and paste this code
App/Lib/Config.php
This code reads the Array from 
config.php
 and checks if the key exists in the array if so return the value otherwise return the default value given
Let’s check if working by editing the 
index.php
 adding these lines
<?php require __DIR__ . '/vendor/autoload.php';

// New lines
use App\Lib\Config;
$LOG_PATH = Config::get('LOG_PATH', '');
echo "[LOG_PATH]: $LOG_PATH";
now run 
php index.php
 and should output the path of the logs specified on
config.php
It seems not much but at this point, you should be getting an idea how the rest of the code will work, we’ll add some classes into 
App
 folder and thanks to the autoloading will be accessible anywhere in the app.
So if you manage to follow along until here, congrats! grab some coffee and let’s continue.

Adding Logging

Earlier we added the 
monolog
 package to our dependencies, this package contains a series of classes and helpers to manage logs. Logging is an essential part of any app since it will be the first thing you check when anything goes wrong and packages like monolog make this job easier and even the possibility to send those via email, slack, telegram, you name it!, for this app, I want to create three simple log files 
errors.log
,
requests.log
 and 
app.log
errors and requests logs will be active all the time and app logs will be used on demand for us to display desire information, 
errors.log
 will contain any error that happens in the app, 
requests.log
 will log any HTTP request made to the app
create 
App/Lib/Logger.php
 and paste the code below, this will be a wrapper that will manage our different logs
App/Lib/Logger.php
now we have two main functions 
Logger::enableSystemLogs()
 this will enable our error/request logs, and then we have 
Logger::getInstance()
that by default will be our App log, let’s try it, modify our 
index.php
 once again with these new lines
<?php require __DIR__ . '/vendor/autoload.php';

use App\Lib\Config;
$LOG_PATH = Config::get('LOG_PATH', '');
echo "[LOG_PATH]: $LOG_PATH";

//New Lines
use App\Lib\Logger;

Logger::enableSystemLogs();
$logger = Logger::getInstance();
$logger->info('Hello World');
type 
php -S localhost:8000
 it’ll run a built-in web server that is present in PHP since 5.4, navigate to
http://localhost:8000
, you should see the “
LOG_PATH
”, but if you check your logs folder you will see two files, showing the requested content and another one with
“Hello World”
text, take a time to tweak the request if you need to show specific info or remove it, this was meant to show different types of logging
finally lets clean a little bit our 
index.php
 and create a new file called
App/Lib/App.php
 , let’s use this as a bootstrap to our app
App/Lib/App.php
and update the index.php
<?php require __DIR__ . '/vendor/autoload.php';
use App\Lib\App;

App::run();
looks much nicer right?

Adding Routing

In any modern app, routing takes a huge part of it, this will call a specific code based on the path in the URL we choose, for example 
/
 could show the homepage, 
/post/1
 could show the post information with id 1, for this we will implement three classes 
Router.php
 , 
Request.php
 and
Response.php
Our 
Router.php
 will be very basic, it will verify the request method and match the path we are giving using regex, if match, it will execute a callback function given by us with two parameters 
Request
 and 
Response
Request.php
will have some functions to get the data that was sent in the request, for example, the Post data such as title, body to create it,
Response.php
 will have some functions to output as JSON with specific HTTP status
create 
App/Lib/Router.php
 , 
App/Lib/Request.php
 , 
App/Lib/Response.php
App/Lib/Router.php
App/Lib/Request.php
App/Lib/Response.php
update your 
index.php
 with the code below,
index.php
type 
php -S localhost:8000
 to test it and navigate to http://localhost:8000/ and you should see ‘Hello World’ and
http://localhost:8000/post/1,
you should see a JSON response with status ‘ok’ and the id you gave inside ‘Post’
{"status": "ok", "post": { "id" : 1} }
if you are using Apache you might need to add this 
.htaccess
 file to the root of your project
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+)$ index.php [QSA,L]
in the case of Nginx
location / {
  try_files $uri $uri/ /index.php$is_args$args;
}
Great! our app now has routing! is time to take a break again, go and grab some lunch! , You want to continue? ok as a bonus let’s add a really simple Controller, this might be useful in the future if you want to use a template engine like Twig
create 
App/Controller/Home.php
App/Controller/Home.php
and modify the 
Router::get('/',..)
 in the 
index.php
 with
use App\Controller\Home;

Router::get('/', function () {
   (new Home())->indexAction();
});

Implementing our Blog API

Finally!, we are almost over!, in these steps, we are finally implementing our Blog API, thanks to our Router, the next steps will be easy,
We will have three endpoints
  • GET 
    /post 
    , list all the available post
  • POST
     /post
     , Create a new Post
  • GET 
    /post/{id}
     show and specific post
First, we need our 
Posts
 model to handle these operations and then be called from our router
create 
App/Model/Posts.php
App/Model/Posts.php
create a 
db.json
 file in the root of the project and paste this so we can have a content already to test
[
   {
     "id": 1,
     "title": "My Post 1",
     "body": "My First Content"
   }
]
modify our 
config.php 
to add the 
DB_PATH
<?php
return [
 'LOG_PATH' => __DIR__ . './logs',
 'DB_PATH' => __DIR__ . '/db.json'
];
with this we already have our “DB” setup, now we need to use it with our router, let’s modify our 
index.php
 to add the routes and DB call respectively
index.php
in this step, we added 
Posts::load() 
to load our “DB” from the 
db.json
file and created three routes GET 
/post
 to list, POST 
/post
 to create and GET
/post/([0–9]*)
 to get a specific post, you could move the
Posts::load()
 inside our 
App::run
 method to make it cleaner.
Great! let’s test it!, you could use postman, curl, to simulate the POST request
List all posts 
curl -X GET 
http://localhost:8000/post
 should output:
[{"id":1,"title":"My Post 1","body":"My First Content"}]
List one post 
curl -X GET 
http://localhost:8000/post
/1
 should output:
{"id":1,"title":"My Post 1","body":"My First Content"}
Create a post
curl -X POST \
 http://localhost:8000/post \
 -H 'Content-Type: application/json' \
 -d '{"title": "Hello World", "body": "My Content"}'
Finally! is finished! we have our Blog Api working! if you manage to follow along until here and you didn’t get bored, Congrats once again!, but before we wrap up, let’s add some testing and I promise we’ll finish

Adding Testing

Ok, we got this far, so let’s implement some testing, for this step, I will test only our 
Router.php
 with simple cases and the code styling based on
psr-2
 coding style standard, but you should take the time to test as much you can in your app, my intention is just to show you how to add this into our app and CI
we need to add some package into our project, type
composer require --dev squizlabs/php_codesniffer
composer require --dev peridot-php/peridot
composer require --dev peridot-php/leo
composer require --dev eloquent/phony-peridot
run in the terminal 
./vendor/bin/phpcs — standard=psr2 App/
 to check if any code syntax is wrong, this will be part of our test script, but try to run it now, in case you have only white-spaces errors, you could use
./vendor/bin/phpcbf — standard=psr2 App/
 to fix it automatically
for unit testing, we are going to use my personal choice peridot but you could use any you feel comfortable, besides peridot, we have two plugins,
leo
 provides expect functionality and 
phony-peridot
 provides stubs functionality that is very handy to check if a function was called
create 
Test/Router.spec.php
Test/Router.spec.php
modify the 
composer.json
 and add this section below
"scripts": {
   "test": [
     "./vendor/bin/phpcs --standard=psr2 App/",
     "./vendor/bin/peridot Test/"
   ]
}
now to run the test, you could just type 
./vendor/bin/peridot Test/
 or
composer run-script test
 or even shorter with 
composer test
 , all of them would do the same if everything went right you see this

Conclusions

This was a very simple project and a lot of things was left out to keep the article shorter as possible, but you could use it as a base and extended it by adding a better router, an ORM, template engine and, so forth, take time and check https://packagist.org/explore/popular
Nice to read:

Written by dhgouveia | Senior full stack web developer
Published by HackerNoon on 2020/09/28