Slim codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the RealWorld spec and API.
This codebase was created to demonstrate a fully fledged fullstack application built with Slim including CRUD operations, authentication, routing, pagination, and more.
We've gone to great lengths to adhere to the Slim community styleguides & best practices.
For more information on how to this works with other frontends/backends, head over to the RealWorld repo.
The basic idea behind this app is to provide a backend web service for the website Conduit made by the Thinkster team.
It is designed as an api which process requests and return JSON responses.
tl;dr commands
git clone https://github.com/gothinkster/slim-php-realworld-example-app.git
cd slim-php-realworld-example-app
composer install
Edit
.env
file with your database configurations.
composer refresh-database
composer start
visit http://localhost:8080/api/articles from your browser
Or follow this detailed guide to install and understand the code
Make sure you have php, mysql and composer installed on your machine.
You should start by cloning the repository into your local machine.
git clone https://github.com/gothinkster/slim-php-realworld-example-app.git
cd slim-php-realworld-example-app
The app is built using Slim. However, there ara extra packages used by the app must be installed.
Install Dependencies
composer install
List of Dependencies
- tuupola/slim-jwt-auth To manage the api authentication using JWT.
- respect/validation Validate the request parameters.
- league/fractal Transfer data models to JSON for the Api responses.
- illuminate/database Eloquent ORM for ActiveRecord implementation and managing data models
- robmorgan/phinx Manage database migration.
- vlucas/phpdotenv To load environment variables from
.env
file. - fzaninotto/faker Generate fake data for testing and populating the database.
- phpunit/phpunit Testing Framework.
All the app environments variables are stored in the .env file.
The command composer install
will copy .env.example to .env
which should have your own variables and never shared or committed into git.
Check the .env file and edit the variables according to your environments. (Database name/user/password). The
.env
is loaded usingvlucas/phpdotenv
through these lines of code
Open the project directory using your favorite editor.
The app follows the structure of Slim skeleton application with minor changes. The skeleton is a good starting point when developing with Slim framework. A detailed overview of the directory structure can be found at this page.
The api is designed according to the RealWorld specifications. Make sure to familiarized yourself with all endpoints specified in the Full API Spec
The code utilizes the MVC pattern where requests are redirected to a controller to process the request and returned a JSON response. Persistence of data is managed by the models which provide the source of truth and the database status.
Database Schema
The app is built using a relational database (e.g. MySQL), and consists of 8 tables.
Setup the database Create a database in MySQL and name it
conduit
or whatever you prefer. Next, don't forget to update Environments Variables in.env
file. Check Database Documentation for details on the schema.
Database Migration:
Database migrations or Schema migration is where the app defines the database structure and blueprints. It also holds the history of any changes made to the database schema and provides easy way to rollback changes to older version.
The app database migrations can be found at the migration directory. Migrations are performed using Phinx.
Migrate the Database
To create all the tables using migration run the following command from the project directory.
php vendor/bin/phinx migrate
Data Models
The data is managed by models which represent the business entities of the app. There are four models User
, Article
, Comment
, and Tag
.
They can be found at Models Directory. Each model has corresponding table in the database.
These models extends Illuminate\Database\Eloquent\Model
which provides the ORM implementations.
Relationships with other models are defined by each model using Eloquent.
For example, User-Comment
is a one-to-many relationship
which is defined by the User model
and by the Comment model.
This relationship is stored in the database by having a foreign key user_id
in the comments table.
Beside The four tables in the database representing each model, the database has three other tables to store many-to-many relationships (article_tag
, user_favorite
, users_following
).
For example, An article can have many tags, and a tag can be assigned to many articles. This relationship is defined by the
Article model
and the Tag model,
and is stored in the table article_tag
.
Data Seeding To populate the database with data for testing and experimenting with the code. Run:
php vendor/bin/phinx migrate
php vendor/bin/phinx seed:run
To edit how the data is seeded check the file: DataSeeder.
The command
composer refresh-database
will rollback all migrations, migrate the database and seed the data. (Note: all data will be lost from the database)
Start the app by running the following command:
composer start
This command will spin a local php server which is enough for testing. You can check the api by visiting http://localhost:8080/api/articles
To check all endpoints you need an HTTP client e.g Postman. There is Postman collection made by Thinkster team you could use.
The server will direct all requests to index.php. There, we boot the app by creating an instance of Slim\App and require all the settings and relevant files.
Finally, we run the app by calling $app->run()
, which will process the request and send the response.
We include four important files into the index.php:
settings.php
,dependencies.php
,middleware.php
,routes.php
I's a good idea to check them before continuing.
The instance of Slim\App ($app
) holds the app settings, routes, and dependencies.
We register routes and methods by calling methods on the $app
instance.
More importantly, the $app
instance has the Container
which register the app dependencies to be passed later to the controllers.
In different parts of the application we need to use other classes and services. These classes and services also depends on other classes. Managing these dependencies becomes easier when we have a container to hold them. Basically, we configure these classes and store them in the container. Later, when we need a service or a class we ask the container, and it will instantiate the class based on our configuration and return it.
The container is configured in the dependencies.php.
We start be retrieving the container from the $app
instance and configure the required services:
$container = $app->getContainer();
$container['logger'] = function ($c) {
$settings = $c->get('settings')['logger'];
$logger = new Monolog\Logger($settings['name']);
$logger->pushProcessor(new Monolog\Processor\UidProcessor());
$logger->pushHandler(new Monolog\Handler\StreamHandler($settings['path'], $settings['level']));
return $logger;
};
The above code registers a configured instance of the logger
in the container. Later we can ask for the logger
$logger = $container->get('logger');
$logger->info('log message');
We register two middleware with the container:
We will see them in action later in the Authentication section.
// Jwt Middleware
$container['jwt'] = function ($c) {
$jws_settings = $c->get('settings')['jwt'];
return new \Slim\Middleware\JwtAuthentication($jws_settings);
};
// Optional Auth Middleware
$container['optionalAuth'] = function ($c) {
return new OptionalAuth($c);
};
The above services example will not be instantiated until we call for them. However, we could use a service provider to have some services available without retrieving them from the container.
Our app use Eloquent ORM to handle our data models. Eloquent must be configured and booted. We do this in the EloquentServiceProvider class, and register the service provider with the container.
$container->register(new \Conduit\Services\Database\EloquentServiceProvider());
For more details check Dependency Container documentations.
All requests go through the same cycle: routing > middleware > conroller > response
Check the list of endpoints defined by the RealWorld API Spec
All the app routes are defined in the routes.php file.
The Slim $app
variable is responsible for registering the routes.
You will notice that all routes are enclosed in the group
method which gives the prefix api to all routes: http::/localhost/api
Every route is defined by a method corresponds to the HTTP verb. For example, a post request to register a user is defined by:
$this->post('/users', RegisterController::class . ':register')->setName('auth.register');
Notice: we use
$this
because where are inside a closure that is bound to$app
;
The method, post()
, defines /api/users
endpoint and direct the request to method register
on RegisterController
class.
In a Slim app, you can add middleware to all incoming routes, to a specific route, or to a group of routes. Check the documentations
In this app we add some middleware to specific routes. For example, to access /api/articles
POST endpoint, the request will go through $jwtMiddleware
$this->post('/articles', ArticleController::class . ':store')->add($jwtMiddleware)->setName('article.store');
see Authentication for details
Also, We add some global middleware to apply to all requests in middleware.php. CORS Middleware for example.
see CORS for details
After passing through all assigned middleware, the request will be processed by a controller.
Note: You could process the request inside a closure passed as the second argument to the method defining the route. For example, the last route, which is left as an example from the skeleton project, handles the request in a closure Check the documentations.
The controller's job is to validate the request data, check for authorization, process the request by calling a model or do other jobs, and eventually return a response in the form of JSON response.
// TODO : Explain how dependencies are injected to the controller.
The api routes can be open to the public without authentication e.g Get Article.
Some routes must be authenticated before being processed e.g Follow user.
Other routes require optional authentication and can be submitted without authentication.
However, when the request has a Token
, the request must be authenticated.
This will make a difference in that the request's user identity will be know when we have an authenticated user, and the response must reflect that.
For example, the Get Profile
endpoint has an optional authentication. The response will be a profile of a user,
and the value of following
in the response will depend on whether we have aToken
in the request.
Unlike traditional web application, when designing a RESTful Api, when don't have a session to authenticate. On popular way to authenticate api requests is by using JWT.
The basic workflow of JWT is that our application will generate a token and send it with the response when the user sign up or login. The user will keep this token and send it back with any subsequent requests to authenticate his request. The generated token will have header, payload, and a signature. It also should have an expiration time and other data to identify the subject/user of the token. For more details, the JWT Introduction is a good resource.
Dealing with JWT is twofold:
- Generate a JWT and send to the user when he sign up or login using his email/password.
- Verify the validity of JWT submitted with any subsequent requests.
We generate the Token when the user sign up or login using his email/password. This is done in the RegisterController and LoginController by the Auth service class.
Review Container Dependencies about the auth service.
Finally, we send the token with the response back to the user/client.
To verify the JWT Token we are using tuupola/slim-jwt-auth library. The library provides a middleware to add to the protected routes. The documentations suggest adding the middleware to app globally and define the protected routes. However, in this app, we are taking slightly different approach.
We add a configured instance of the middleware to the Container, and then add the middleware to every protected route individually.
Review Container Dependencies about registering the middleware.
In the routes.php file,
we resolve the middleware out of the container and assign to the variable $jwtMiddleware
Then, when defining the protected route, we add the middleware using the add
method:
$jwtMiddleware = $this->getContainer()->get('jwt');
$this->post('/articles', ArticleController::class . ':store')->add($jwtMiddleware)
The rest is on the tuupola/slim-jwt-auth
to verify the token.
If the token is invalid or not provided, a 401 response will be returned.
Otherwise, the request will be passed to the controller for processing.
For the optional authentication, we create a custom middleware OptionalAuth. The middleware will check if there a token present in the request header, it will invoke the jwt middleware to verify the token.
Again, we use the OptionalAuth middleware by store it in Container and retrieve it to add to the optional routes.
$optionalAuth = $this->getContainer()->get('optionalAuth');
$this->get('/articles/feed', ArticleController::class . ':index')->add($optionalAuth);
Some routes required authorization to verify that user is authorized to submit the request. For example, when a user wants to edit an article, we need to verify that he is the owner of the article.
The authorization is handled by the controller. Simply, the controller will compare the article's user_id with request's user id. If not authorized, the controller will return a 403 response.
if ($requestUser->id != $article->user_id) {
return $response->withJson(['message' => 'Forbidden'], 403);
}
However, in a bigger application you might want to implement more robust authorization system.
CORS is used when the request is coming from a different host.
By default, web browsers will prevent such requests.
The browser will start by sending an OPTION
request to the server to get the approval and then send the actual request.
Therefor, we handle cross-origin HTTP requests by making two changes to our app:
- Allow
OPTIONS
requests. - Return the approval in the response.
This is done in the by adding two middleware in the middleware.php file The first middleware will add the required headers for CORS approval. And the second, deals with issue of redirect when the route ends with a slash.
For more information check Slim documentations:
composer test