Since the introduction of package manager and the , 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. Composer PHP standards 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 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 id, title, and body, 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. All code is available in https://gitlab.com/dhgouveia/medium-blog-api 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 needed to add 3rd party packages and manage our project with the feature, this will make importing classes easier. composer.json autoloading Create a folder and type in your terminal and fill the information, it will create the file for us, then create our basic folder structure with some empty files called , and an empty folder called composer init composer.json index.php config.php App Let’s add the first package by using the command line , it creates a folder with the package we just added and a file called , this file will contain all the path to the classes we add from 3rd parties and ours, is a package to create logs files that will be used later on composer require monolog/monolog:1.25.1 vendor autoload.php monolog Open and fill it with: index.php . ; <?php require __DIR__ '/vendor/autoload.php' modify the by adding the autoload entry after the type entry composer.json : , : { : { : } }, "type" "project" "autoload" "psr-4" "App\\" "App/" then type to update the autoload entries, the entry will register all our classes to be used anywhere in our app, is a more flexible autoloading standard specification than , you don’t need to regenerate the autoloader when you add classes for example. composer dump-autoload autoload psr-4 psr-0 By now, the app is already setup to work with composer, you can run in the terminal, if no error is shown it means is working, this shouldn't output anything php index.php Adding our first class Let’s make a Config helper to use across the project, we are going to have 2 files, 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 to read those variables config.php App/Lib/Config.php Open and fill it with: config.php [ => . , ]; <?php return 'LOG_PATH' __DIR__ './logs' create a new file inside called it and paste this code App/Lib/ Config.php App/Lib/Config.php This code reads the Array from and checks if the key exists in the array if so return the value otherwise return the default value given config.php Let’s check if working by editing the adding these lines index.php . ; \ \ ; $LOG_PATH = Config::get( , ); ; <?php require __DIR__ '/vendor/autoload.php' // New lines use App Lib Config 'LOG_PATH' '' echo "[LOG_PATH]: $LOG_PATH" now run and should output the path of the logs specified on php index.php 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 folder and thanks to the autoloading will be accessible anywhere in the app. App So if you manage to follow along until here, congrats! grab some coffee and let’s continue. Adding Logging Earlier we added the 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 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 , and monolog monolog errors.log requests.log app.log and logs will be active all the time and logs will be used on demand for us to display desire information, will contain any error that happens in the app, will log any HTTP request made to the app errors requests app errors.log requests.log create and paste the code below, this will be a wrapper that will manage our different logs App/Lib/Logger.php App/Lib/Logger.php now we have two main functions this will enable our error/request logs, and then we have that by default will be our App log, let’s try it, modify our once again with these new lines Logger::enableSystemLogs() Logger::getInstance() index.php . ; \ \ ; $LOG_PATH = Config::get( , ); ; \ \ ; Logger::enableSystemLogs(); $logger = Logger::getInstance(); $logger->info( ); <?php require __DIR__ '/vendor/autoload.php' use App Lib Config 'LOG_PATH' '' echo "[LOG_PATH]: $LOG_PATH" //New Lines use App Lib Logger 'Hello World' type it’ll run a built-in web server that is present in PHP since 5.4, navigate to , you should see the “ ”, but if you check your logs folder you will see two files, showing the requested content and another one with 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 php -S localhost:8000 http://localhost:8000 LOG_PATH “Hello World” finally lets clean a little bit our and create a new file called , let’s use this as a bootstrap to our app index.php App/Lib/App.php App/Lib/App.php and update the index.php . ; \ \ ; App::run(); <?php require __DIR__ '/vendor/autoload.php' use App Lib App 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, could show the post information with id 1, for this we will implement three classes , and / /post/1 Router.php Request.php Response.php Our 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 and Router.php Request Response will have some functions to get the data that was sent in the request, for example, the Post data such as , to create it, Request.php title body will have some functions to output as JSON with specific HTTP status Response.php 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 with the code below, index.php index.php type to test it and navigate to and you should see ‘Hello World’ and you should see a JSON response with status ‘ok’ and the id you gave inside ‘Post’ php -S localhost:8000 http://localhost:8000/ http://localhost:8000/post/1, { : , : { : } } "status" "ok" "post" "id" 1 if you are using Apache you might need to add this file to the root of your project .htaccess / !-d !-f ^(.+)$ index.php RewriteEngine On RewriteBase RewriteCond %{REQUEST_FILENAME} RewriteCond %{REQUEST_FILENAME} RewriteRule [QSA,L] in the case of Nginx / { / /index.php ; } location try_files $uri $uri $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 in the with Router::get('/',..) index.php \ \ ; Router::get( , { ( Home())->indexAction(); }); use App Controller Home '/' function () new 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 , list all the available post /post POST , Create a new Post /post GET show and specific post /post/{id} First, we need our model to handle these operations and then be called from our router Posts create App/Model/Posts.php App/Model/Posts.php create a file in the root of the project and paste this so we can have a content already to test db.json [ { : , : , : } ] "id" 1 "title" "My Post 1" "body" "My First Content" modify our to add the config.php DB_PATH [ => . , => . ]; <?php return 'LOG_PATH' __DIR__ './logs' 'DB_PATH' __DIR__ '/db.json' with this we already have our “ ” setup, now we need to use it with our router, let’s modify our to add the routes and DB call respectively DB index.php index.php in this step, we added to load our “ ” from the file and created three routes to list, to create and to get a specific post, you could move the inside our method to make it cleaner. Posts::load() DB db.json GET /post POST /post GET /post/([0–9]*) Posts::load() App::run Great! let’s test it!, you could use , curl, to simulate the POST request postman List all posts should output: curl -X GET http://localhost:8000/post [{ : , : , : }] "id" 1 "title" "My Post 1" "body" "My First Content" List one post should output: curl -X GET http://localhost:8000/post /1 { : , : , : } "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 with simple cases and the code styling based on 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 Router.php psr-2 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 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 to fix it automatically ./vendor/bin/phpcs — standard=psr2 App/ ./vendor/bin/phpcbf — standard=psr2 App/ for unit testing, we are going to use my personal choice but you could use any you feel comfortable, besides peridot, we have two plugins, provides functionality and provides functionality that is very handy to check if a function was called peridot leo expect phony-peridot stubs create Test/Router.spec.php Test/Router.spec.php modify the and add this section below composer.json : { : [ , ] } "scripts" "test" "./vendor/bin/phpcs --standard=psr2 App/" "./vendor/bin/peridot Test/" now to run the test, you could just type or or even shorter with , all of them would do the same if everything went right you see this ./vendor/bin/peridot Test/ composer run-script test composer test 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 All code is available in https://gitlab.com/dhgouveia/medium-blog-api Nice to read: https://www.php-fig.org/ https://php7.org/guidelines https://phptherightway.com Also published at https://medium.com/@dhgouveia/write-modern-php-without-framework-d244d8ca2b50