andrewdyer/slim3-restful-api-web-seed

A basic starter structure which can be used to develop RESTful APIs and web applications, built with the Slim 3 framework.

v1.2.1 2018-05-15 16:32 UTC

README

Codacy Badge

A basic starter structure which can be used to develop RESTful APIs and web applications, built with the Slim 3 framework.

Index

License

Licensed under MIT. Totally free for private or commercial projects.

Requirements

  • PHP 7.1.14+
  • MySQL 5.7.20+
  • Composer

Installation

composer create-project andrewdyer/slim3-restful-api-web-seed project_name

Configuration

  • Activate mod_rewrite, route all traffic to application's /public directory.
  • Set up the project environment by updating the .env file in the application's root directory.
  • Run all available migrations.

Documentation

Skeleton files for each of the following components can be found in resource/templates.

Controllers

This project provides controller functionality to Slim. Controllers are typically stored in the app/Controllers directory, however they can technically live in any directory or any sub-directory. All controllers should extend the App\Controllers\AbstractController class, which is used as a place to put shared controller logic.

Here's a basic usage example of a controller:

app/Controllers/ArticlesController.php

namespace App\Controllers;
    
use App\Models\Article;
use App\Controllers\AbstractController;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
    
class ArticlesController extends AbstractController
{  
    
    /**
     * 
     * @param Request $request
     * @param Response $response
     * @return Response
     */
    public function getAction(Request $request, Response $response)
    {
        return $this->render($response, "articles/create.html.twig");
    }
    
    /**
     * 
     * @param Request $request
     * @param Response $response
     * @return Response
     */
    public function postAction(Request $request, Response $response)
    {
        $article = new Article;
        $article->title = $request->getParam("input");
        $article->content = $request->getParam("content");
        $article->save();

        return $response->withRedirect($this->pathFor("articles.create"));
    }
    
}

For most web application controllers, you will want to render a view. Here is a basic example of a twig view. For how to handle API responses see the presenters documentation:

resource/views/articles/create.html.twig

{% extends 'base.html.twig' %}
    
{% block content %}
    <form method="post">
        <div class="form-group {{errors.title ? "has-error" : ""}}">
            <label for="title-input" class="control-label">Title</label>
            <input type="text" id="title-input" class="form-control" name="title" value="{{input.title}}" />
            {% if errors.title %}
                <span class="help-block">{{errors.title | first}}</span>
            {% endif %}
        </div>
        <div class="form-group {{errors.content ? "has-error" : ""}}">
            <label for="content-input" class="control-label">Content</label>
            <textarea id="content-input" class="form-control" name="content">{{input.content}}</textarea>
            {% if errors.content %}
                <span class="help-block">{{errors.content | first}}</span>
            {% endif %}
        </div>
        <button type="submit" class="btn btn-default">Create Post</button>
    </form>
{% endblock %}

Don't forget to define your routes, passing the controller class name and method as the callable:

routes/web.php

$app->get("/articles", App\Controllers\ArticlesController::class . ":getAction")->setName("articles.create");
$app->post("/articles", App\Controllers\ArticlesController::class . ":postAction")->setName("articles.create");

In summary, the above example will see all GET requests made to the /articles endpoint call the getAction() method of the class ArticlesController, which will simply render a twig view. When the submit button is clicked on the view, a POST request will be made to the same endpoint, which is handled by the postAction() of the ArticlesController class. This method will create a new model and redirect the user back the same page.

Models

This project makes use of Eloquent ORM, a simple ActiveRecord implementation for working with databases. Each database table has a corresponding "Model" which is used to interact with that table. Models allow you to query for data in tables, as well as insert new records into the table. Models are typically stored in the app/Models directory, but you are free to place them anywhere that can be auto-loaded. All models are required to extend the App\Models\AbstractModel class.

Here's a basic usage example of a model:

app/Models/Article.php

namespace App\Models;
    
use App\Models\AbstractModel;
    
class Article extends AbstractModel
{
    
    /**
     *
     * @var string 
     */
    protected $table = "articles";
    
}

Presenters

Presenters are used to generate the view data. They are basically a class that accepts a model and wraps it in some specific logic to alter the returned values without having to modify the original object. A presenter should not do any data manipulation, but can contain model calls and any other retrieval or preparation operations needed to generate the view data. Presenters are typically stored in the app/Presenters directory, although can be placed anywhere that can be auto-loaded, and are required to extend the App\Presenters\AbstractPresenter class.

Here's a basic usage example of a presenter:

app/Presenters/ArticlePresenter.php

namespace App\Presenters;
    
use App\Presenters\AbstractPresenter;
    
class ArticlePresenter extends AbstractPresenter
{
    
    /**
     * 
     * @param object $data
     * @return array
     */
    public function format($data): array
    {
        return [
            "id" => $data->id,
            "title" => $data->title,
            "excerpt" => substr($data->content, 0, 145),
            "content" => $data->content
        ];
    }
    
}

In the controller, instead of rendering a twig view, you can pass into $response->withJson() a new instance of the presenter - calling the ->present() method on it:

app/Controllers/ArticlesController.php

namespace App\Controllers;
    
use App\Models\Article;
use App\Controllers\AbstractController;
use App\Presenters\ArticlePresenter;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
    
class ArticlesController extends AbstractController
{
    
    /**
     * 
     * @param Request $request
     * @param Response $response
     * @return Response
     */
    public function getAction(Request $request, Response $response)
    {
        $article = Article::find(1);
        
        return $response->withJson((new ArticlePresenter($article))->present());
    }
    
}

Middleware

Middleware is code that is run before and after your application to manipulate the Request and Response objects as you see fit. Although Middleware can be placed anywhere that can be auto-loaded, it is typically stored in app/Middleware and should extend the App\Middleware\AbstractMiddleware class.

Here's a basic usage example of middleware:

app/Middleware/ExampleMiddleware.php

namespace App\Middleware;
    
use App\Middleware\AbstractMiddleware;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
    
class ExampleMiddleware extends AbstractMiddleware
{
    
    /**
     * 
     * @param Request $request
     * @param Response $response
     * @param callable $next
     * @return Response
     */
    public function handle(Request $request, Response $response, callable $next): Response
    {
        $response->getBody()->write('BEFORE');
        $response = $next($request, $response);
        $response->getBody()->write('AFTER');

        return $response;
    }
    
}

To register middleware, you need to use the ->add() function chain in bootstrap/middleware.php or against specific routes in routes/ directory.

bootstrap/middleware.php

$container = $app->getContainer();
$app->add(new App\Middleware\ExampleMiddleware($container));

Commands

Commands are typically stored in the app/Commands directory and are defined in classes extending the App\Commands\AbstractCommand class.

Here's a basic usage example of a command:

app/Commands/SayHelloCommand.php

namespace App\Commands;
    
use App\Commands\AbstractCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
    
class SayHelloCommand extends AbstractCommand
{
    
    /**
     * 
     * @return array
     */
    public function arguments(): array
    {
        return [
            ["name", InputArgument::OPTIONAL, "Your name"],
        ];
    }
    
    /**
     * 
     * @return string
     */
    public function description(): string
    {
        return "";
    }
    
    /**
     * 
     * @param InputInterface $input
     * @param OutputInterface $output
     * @return mixed
     */
    public function handle(InputInterface $input, OutputInterface $output)
    {
        for ($i = 0; $i < $input->getOption("repeat"); $i ++) {
            $output->writeln("<comment>" . "Hello " . $input->getArgument("name") . "</comment>");
        }
    }
    
    /**
     * 
     * @return string
     */
    public function help(): string
    {
        return "";
    }
    
    /**
     * 
     * @return string
     */
    public function name(): string
    {
        return "say:hello";
    }
    
    /**
     * 
     * @return array
     */
    public function options(): array
    {
        return [
            ["repeat", "r", InputOption::VALUE_OPTIONAL, "Times to repeat output", 1]
        ];
    }
    
}

Commands are registered by using the ->add(); function chain in bootstrap/commands.php.

config/commands.php

$container = $app->getContainer();
$console->add(new App\Commands\SayHelloCommand($container));

Migrations

Phinx is a database migration tool. You can tell Phinx that you want to create a new database table, add a column or edit the properties of a column by writing “migrations”. Each migration is represented by a PHP class in a unique file.

Phinx is run using a number of commands in the projects root directory;

Create Command

The create command is used to create a new migration file. The file will be located in the app/Migrations directory and will contain a skeleton migration class. The command requires one argument: the name of the migration. The migration name should be specified in CamelCase format.

php vendor/bin/phinx create MyNewMigration

Migrate Command

The migrate command runs all of the available migrations, optionally up to a specific version.

php vendor/bin/phinx migrate

To migrate to a specific version then use the --target parameter or -t for short.

php vendor/bin/phinx  migrate -e development -t 20110103081132

Use --dry-run to print the queries to standard output without executing them

php vendor/bin/phinx  migrate --dry-run

Rollback Command

The Rollback command is used to undo previous migrations executed by Phinx. It is the opposite of the migrate command. You can rollback to the previous migration by using the rollback command with no arguments.

php vendor/bin/phinx rollback -e development

To rollback all migrations to a specific version then use the --target parameter or -t for short.

php vendor/bin/phinx rollback -e development -t 20120103083322

Specifying 0 as the target version will revert all migrations.

php vendor/bin/phinx rollback -e development -t 0

To rollback all migrations to a specific date then use the --date parameter or -d for short.

php vendor/bin/phinx rollback -e development -d 2012
php vendor/bin/phinx rollback -e development -d 201201
php vendor/bin/phinx rollback -e development -d 20120103
php vendor/bin/phinx rollback -e development -d 2012010312
php vendor/bin/phinx rollback -e development -d 201201031205
php vendor/bin/phinx rollback -e development -d 20120103120530

If a breakpoint is set, blocking further rollbacks, you can override the breakpoint using the --force parameter or -f for short.

php vendor/bin/phinx rollback -e development -t 0 -f

Use --dry-run to print the queries to standard output without executing them

php vendor/bin/phinx rollback --dry-run

JWT Authentication

Get a Token

curl -d "username=$USERNAME&password=$PASSWORD" -X POST http://localhost:8000/api/auth
HTTP/1.1 201 Created
Content-Type: application/json
{
    "token": "XXXXXX",
    "expires": 1234567890
}

Validation

This project provides a simple way to validate data which has been submitted to the server by the user.

namespace App\Controllers\Api;
    
use App\Controllers\AbstractController;
use App\Models\User;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Respect\Validation\Validator as v;
    
class ExampleController extends AbstractController
{
    
     /**
     * 
     * @param Request $request
     * @param Response $response
     * @return Response
     */
    public function postAction(Request $request, Response $response)
    {
        $validation = $this->validate($request, [
            "email" => v::email()->length(1, 254)->notEmpty(),
            "forename" => v::alpha()->length(1, 100)->notEmpty()->noWhitespace(),
            "password" => v::length(8, 100)->notEmpty(),
            "surname" => v::alpha()->length(1, 100)->notEmpty()->noWhitespace(),
            "username" => v::alnum()->length(1, 32)->notEmpty()->noWhitespace()
        ]);
        

        if(!$validation->hasPassed()) {
            return $response->withJson([
                "errors" => $validation->getErrors()
            ], 400);
        }
        
        $user = new User;
        $user->email = $request->getParam("email");
        $user->forename = $request->getParam("forename");
        $user->password = $request->getParam("password");
        $user->surname = $request->getParam("surname");
        $user->username = $request->getParam("username");
        $user->save();
        

        return $response->withJson([
            "message" => "User was created!"
        ]);
    }
        
}

You can easily validate your form inputs using the validate() helper. Accessed exclusively in a controller, assign to a variable the validate instance passing in the $request object as well as an array where the array key represents the name of the form input and the array value represents the validation rules:

$validation = $this->validate($request, [
    "forename" => v::alpha()->length(1, 100)->notEmpty()->noWhitespace(),
    "surname" => v::alpha()->length(1, 100)->notEmpty()->noWhitespace(),
    "username" => v::alnum()->length(1, 32)->notEmpty()->noWhitespace()
]);

Respect\Validation is namespaced, but you can make your life easier by importing a single class into your context:

use Respect\Validation\Validator as v;

You can then check if the validation has passed using the hasPassed() method:

if(!$validation->hasPassed()) {
    // Validation has not passed - return an error
} else {
    // Validation has passed - update a model
}

If the validation has failed, an array of the validation errors can be accessed by calling the getErrors() method:

foreach($validation->getErrors() as $input => $errors) {
    foreach($errors as $error) {
        echo $error;
    }
}

In cases where the API is going to be doing the validation, it might not always make sense to submit every form inputs to the server - just those which have been updated. Depending on how and what data your server will be recieving the validate() method can take an optional third argument - a boolean, which when set to true will dynamically grab all the inputs from the request body and skip over any validation rules which are not applicable:

$validation = $this->validate($request, [
    "email" => v::email()->length(1, 254)->notEmpty(),
    "forename" => v::alpha()->length(1, 100)->notEmpty()->noWhitespace(),
    "password" => v::length(8, 100)->notEmpty(),
    "surname" => v::alpha()->length(1, 100)->notEmpty()->noWhitespace(),
    "username" => v::alnum()->length(1, 32)->notEmpty()->noWhitespace()
], true);

As an example based on the above snippet, if the server only receives a username and password it will skip over the validation for email, forename and surname as there is no data to validate. If this was not to be dynamically validated, the validation would fail as email, forename and surname cannot be empty and therefore would need to be present in the request to pass. This third argument will be useful for controllers which can recieve singular or different combination of data - prehaps from a front-end which employes input focus out saving. As a sidenote, if you do choose to use this method of validating inputs you can use the getParams() method to get an array of inputs that were recieved and therefore validated.

Useful Links