In this blog, I write about Web Development, Blockchain, and DevOps. I am a Docker Captain, Public Speaker, and Developer Advocate at daily.dev. Check my YouTube channel.
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.
Video version:
All the code is available in the GitHub repository (link in the video description)
🏁 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 lines 11-16 (DB_CONNECTION, DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD) with the following (it should be mysql by default).
⚠️ 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:
<?phpnamespaceApp\Models;
useIlluminate\Database\Eloquent\Factories\HasFactory;
useIlluminate\Database\Eloquent\Model;
classPlayerextendsModel{
useHasFactory;
//add name and email to fillableprotected $fillable = ['name', 'email'];
}
Open the create_players_table.php file in the database/migrations folder and replace it with the following:
<?phpuseIlluminate\Database\Migrations\Migration;
useIlluminate\Database\Schema\Blueprint;
useIlluminate\Support\Facades\Schema;
returnnewclassextendsMigration{
/**
* Run the migrations.
*/publicfunctionup(): void{
Schema::create('players', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/publicfunctiondown(): void{
Schema::dropIfExists('players');
}
};
Now create a file called PlayerController.php in the App/Http/Controllers folder and add the following:
<?phpnamespaceApp\Http\Controllers;
useApp\Models\Player;
useIlluminate\Http\Request;
classPlayerControllerextendsController{
/**
* Display a listing of the resource.
*/publicfunctionindex()
{
//get all players
$players = Player::all();
//return JSON response with the playersreturn response()->json($players);
}
/**
* Store a newly created resource in storage.
*/publicfunctionstore(Request $request)
{
$validatedData = $request->validate([
'name' => 'required|string',
'email' => 'required|string',
]);
$player = Player::create($validatedData);
return response()->json($player, 201);
}
/**
* Display the specified resource.
*/publicfunctionshow(Player $player)
{
// return JSON response with the playerreturn response()->json($player);
}
/**
* Update the specified resource in storage.
*/publicfunctionupdate(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.
*/publicfunctiondestroy(Player $player)
{
$player->delete();
return response()->json(null, 204);
}
}
Last, open the routes/api.php file and add the following at the top of the file:
<?phpuseIlluminate\Http\Request;
useIlluminate\Support\Facades\Route;
useApp\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.
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 container's working directory.
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.
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. Replace francescoxx with your DockerHub username
build: .: this is the path of the Dockerfile. We will use the Dockerfile that we created in the last 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 an 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.