Let's create a CRUD Rest API in PHP, using:
- Laravel (PHP framework)
- Composer (PHP package manager)
- Postgres (database)
- Docker
- Docker Compose
Mind the similar names!
⚠️ "Composer" is a package manager for PHP. It is used to install and manage dependencies in PHP projects. It is similar to NPM in Node.js projects.
⚠️ "Compose" is a tool for defining and running multi-container Docker applications. It is similar to Docker Compose in Node.js projects.
If you prefer a video version:
All the code is available in the GitHub repository (link in the video description): https://youtube.com/live/cdlHJeHVFW4
🏁 Intro
Here is a schema of the architecture of the application we are going to create:
We will create 5 endpoints for basic CRUD operations:
- Create
- Read all
- Read one
- Update
- Delete
👣 Steps
We will go with a step-by-step guide, so you can follow along.
Here are the steps:
- Check the prerequisites
- Create a new Laravel project
- Code the application
- Run the Postgres database with Docker
- Build and run the application with Docker Compose
- Test the application with Postman and Tableplus
💡 Prerequisites
- php installed (version 8+ )
- composer installed (version 2.5+ )
- docker installed (version 20.10+ )
- [optional] VS Code installed (or any IDE you prefer)
- [optional] Laravel cli
- [optional] Postman or any API test tool
- [optional] Tableplus or any database client
🚀 Create a new Laravel project
To create a new Laravel project, we will use the Laravel CLI.
laravel new laravel-crud-api
This will take a while, but the final output should be something like this:
Now step into the project folder:
cd laravel-crud-api
and open the project with your favorite IDE. If you use VS Code, you can use the following command:
code .
this will open the project, open a terminal and run the following command:
php artisan serve
and you should have something like this:
You can stop the server with Ctrl + C
.
Now we are ready to start coding.
👩💻 Code the application
There are two steps to code the application:
- Configure the database connection
- Create the Player, PlayerController and PLayer routes
🔗 Configure the database connection
We will use Postgres as our database. To configure the database connection, we will use the .env
file.
Open the .env file and replace the lines 11-16 (DB_CONNECTION, DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD) with the following (it should be mysql
by default).
DB_CONNECTION=pgsql
DB_HOST=db
DB_PORT=5432
DB_DATABASE=postgres
DB_USERNAME=postgres
DB_PASSWORD=postgres
Your final .env file should look like this:
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:EB/Rhl0udascxY9GVkIeHpoZT5LtivQlZpVvQp850QQ=
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=pgsql
DB_HOST=db
DB_PORT=5432
DB_DATABASE=postgres
DB_USERNAME=postgres
DB_PASSWORD=postgres
BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
⚠️ Note: note that instead of an ip address, we use the name of the service (db) as the host. This is because we will use Docker Compose to run the application and the database. This is how Docker knows how to connect the two services (of course they should be in the same network).
📁 Create the resource structure
We will create a Player
resource. This resource will have the following fields:
- id (autoincremented)
- name (string)
- email (string)
php artisan make:model Player -m
This created a Player.php file in App/Models and a create_players_table.php file in database/migrations.
Open the Player.php
file and replace it with the following:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Player extends Model
{
use HasFactory;
//add name and email to fillable
protected $fillable = ['name', 'email'];
}
Open the create_players_table.php
file in the database/migrations folder and replace it with the following:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('players', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('players');
}
};
Now create a file called PlayerController.php in the App/Http/Controllers folder and add the following:
<?php
namespace App\Http\Controllers;
use App\Models\Player;
use Illuminate\Http\Request;
class PlayerController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//get all players
$players = Player::all();
//return JSON response with the players
return response()->json($players);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$validatedData = $request->validate([
'name' => 'required|string',
'email' => 'required|string',
]);
$player = Player::create($validatedData);
return response()->json($player, 201);
}
/**
* Display the specified resource.
*/
public function show(Player $player)
{
// return JSON response with the player
return response()->json($player);
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Player $player)
{
$validatedData = $request->validate([
'name' => 'required|string',
'email' => 'required|string',
]);
$player->update($validatedData);
return response()->json($player, 200);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Player $player)
{
$player->delete();
return response()->json(null, 204);
}
}
Last, open the routes/api.php file and add the following at top of the file:
use App\Http\Controllers\PlayerController;
...
And this at the bottom of the file:
...
Route::get('/players', [PlayerController::class, 'index']);
Route::post('/players', [PlayerController::class, 'store']);
Route::get('/players/{player}', [PlayerController::class, 'show']);
Route::put('/players/{player}', [PlayerController::class, 'update']);
Route::delete('/players/{player}', [PlayerController::class, 'destroy']);
The api.php file should look like this:
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PlayerController;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
Route::get('/players', [PlayerController::class, 'index']);
Route::post('/players', [PlayerController::class, 'store']);
Route::get('/players/{player}', [PlayerController::class, 'show']);
Route::put('/players/{player}', [PlayerController::class, 'update']);
Route::delete('/players/{player}', [PlayerController::class, 'destroy']);
🐳 Dockerization
Now let's dockerize the application. We will use docker-compose to run the application.
We will create a Dockerfile and a docker-compose.yml file.
🐋 Dockerfile
Create a new file called Dockerfile
in the root of the project.
Add the following content (explanation is in the comments):
FROM php:8.1
RUN apt-get update && apt-get install -y \
libpq-dev \
&& docker-php-ext-install pdo pdo_pgsql
WORKDIR /var/www/html
COPY . .
RUN chown -R www-data:www-data \
/var/www/html/storage \
/var/www/html/bootstrap/cache
CMD php artisan serve --host=0.0.0.0 --port=8000
Explanation of the Dockerfile:
-
FROM php:8.1
: this is the base image that we will use. We will use the official php image with version 8.1. -
RUN apt-get update && apt-get install -y \
: this is the command that will be executed when the image is built. We will update the apt package manager and install the libpq-dev package. -
RUN docker-php-ext-install pdo pdo_pgsql
: this is the command that will be executed when the image is built. We will install the pdo and pdo_pgsql extensions. -
WORKDIR /var/www/html
: this is the working directory of the container. All the commands will be executed from this directory. -
COPY . .
: this is the command that will be executed when the image is built. We will copy all the files from the current directory to the working directory of the container. -
RUN chown -R www-data:www-data \
: this is the command that will be executed when the image is built. We will change the owner of the storage and bootstrap/cache directories to www-data. -
CMD php artisan serve --host=8000
: this is the command that will be executed when the container is started. We will start the php artisan serve command.
🐙 docker-compose.yml
Let's create the docker-compose.yml
file at the root of the project.
Add the following content:
version: '3'
services:
laravelapp:
container_name: laravelapp
image: francescoxx/laravelapp:1.0.0
build: .
ports:
- "8000:8000"
env_file:
- .env
depends_on:
- db
db:
container_name: db
image: postgres:12
ports:
- "5432:5432"
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data: {}
Explanation of the docker-compose.yml file:
-
version: '3'
: this is the version of the docker-compose file. -
services:
: this is the section where we will define the services that we want to run. -
laravelapp:
: this is the name of the service. -
container_name: laravelapp
: this is the name of the container. -
image: francescoxx/laravelapp:1.0.0
: this is the name of the image that we will use. We will use the image that we created in the previous step. Replacefrancescoxx
with your DockerHub username -
build: .
: this is the path of the Dockerfile. We will use the Dockerfile that we created in the previous step. -
ports:
: this is the section where we will define the ports that we want to expose. -
- "8000:8000"
: this is the port that we want to expose. We will expose the port 8000 of the container to the port 8000 of the host. -
env_file:
: this is the section where we will define the environment variables that we want to use. -
- .env
: this is the path of the .env file. We will use the .env file that we created in the previous step. -
depends_on:
: this is the section where we will define the services that we want to run before this one. -
- db
: this is the name of the service that we want to run before this one. -
db:
: this is the name of the service. -
container_name: db
: this is the name of the container. -
image: postgres:12
: this is the name of the image that we will use. We will use the official postgres image with version 12. -
ports:
: this is the section where we will define the ports that we want to expose. -
- "5432:5432"
: this is the port that we want to expose. We will expose the port 5432 of the container to the port 5432 of the host. -
environment:
: this is the section where we will define the environment variables that we want to use.
Now it's time to build the image and run the services (containers)
Build and run the project
Now we can build and run the project.
💽 Run the Postgres database
First, we need to run the Postgres database.
docker compose up -d db
To check if it's running, you can use the following command:
docker compose logs
and the
docker ps -a
If the output is like the following one, you are good to go:
You should see something like that, you are good to go.
As additional test, you can connect to the database using TablePlus (or any other database client).
You can create a new connection using the following parameters:
- Host: localhost
- Port: 5432
- Database: postgres
- User: postgres
- Password: postgres
Then click on the Test Connection
button. The database is connected but emptt for now.
🏗️ Build the project
To build the project, type:
docker compose build
And the output should be something like that:
🏃♂️ Run the project
Now we can run the project.
docker compose up laravelapp
And this should be the output:
💽 Apply the migrations
Now we need to apply the migrations.
docker compose exec laravelapp php artisan migrate
Now it's time to test the project.
🧪 Test the project
Now we can test the project. We will use Postman, but you can use any other tool.
📝 Create a player
To create a new player, make a POST request to localhost:8000/api/player
.
The body of the request should be like that:
{
"name": "aaa",
"email": "aaa@mail"
}
The output should be something like that:
Let's create two more players, make a POST request to localhost:8000/api/player
.
{
"name": "bbb",
"email": "bbb@mail"
}
{
"name": "ccc",
"email": "ccc@mail"
}
📝 Get all players
To get all players, make a GET request to localhost:8000/api/player
.
The output should be something like that:
📝 Get a player
To get a player, make a GET request to localhost:8000/api/players/{id}
.
For example GET request to localhost:8000/api/players/1
.
The output should be something like that:
📝 Update a player
To update a player, make a PUT request to localhost:8000/api/players/{id}
.
For example PUT request to localhost:8000/api/players/2
.
The body of the request should be like that:
{
"name": "NEW",
"email": "MODIFIED@mail"
}
The output should be something like that:
📝 Delete a player
To delete a user, make a DELETE request to localhost:8000/api/players/{id}
.
For example DELETE request to localhost:8000/api/players/1
.
On Postman you should see something like that:
Final test
As a final test, we can check the database using TablePlus.
Create a new connection using the following parameters:
Host: localhost
Port: 5432
Database: postgres
User: postgres
Password: postgres
Then click on the Connect
button at the bottom-right.
As you can see, we have a players
table with 2 records:
🏁Conclusion
We made it! We have built a CRUD Rest API in PHP, using:
- Laravel (PHP framework)
- Composer (PHP package manager)
- Postgres (database)
- Docker
- Docker Compose
If you prefer a video version:
All the code is available in the GitHub repository (link in the video description): https://youtube.com/live/cdlHJeHVFW4
That's all.
If you have any question, drop a comment below.
Top comments (8)
Hi, thanks for this article.
But i have a question, how can i configure my application if i will need to have 2 or 3 apis like the example provided for you, to communicate between each other?
I use a macOS system, maybe i need to allow some folder permissions?
Thanks in advance
Docker container communicate using networks, so as long as they are on the same network you will find the other containers using the container name (it's more complicated than this, when you get into orchestration, but to test them that' enough)
Hi thanks for great article
I followed all the steps but I have one issue, get player by id return empty array istead of player object
strange, try to clone the github repository (link in the video description)
Nice article
thanks
well done.. built the entire project with no errors
thank you!