DEV Community

Cover image for PHP CRUD Rest API with Docker
Francesco Ciulla
Francesco Ciulla

Posted on

PHP CRUD Rest API with Docker

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:

PHP CRUD Rest API with Laravel, Postgres, Docker and Docker Compose. Postman and Tableplus to test it

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:

  1. Check the prerequisites
  2. Create a new Laravel project
  3. Code the application
  4. Run the Postgres database with Docker
  5. Build and run the application with Docker Compose
  6. 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


Enter fullscreen mode Exit fullscreen mode

This will take a while, but the final output should be something like this:

Laravel project created

Now step into the project folder:



cd laravel-crud-api


Enter fullscreen mode Exit fullscreen mode

and open the project with your favorite IDE. If you use VS Code, you can use the following command:



code .


Enter fullscreen mode Exit fullscreen mode

this will open the project, open a terminal and run the following command:



php artisan serve


Enter fullscreen mode Exit fullscreen mode

and you should have something like this:

Laravel app running

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


Enter fullscreen mode Exit fullscreen mode

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}"



Enter fullscreen mode Exit fullscreen mode

⚠️ 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).

.env file

📁 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


Enter fullscreen mode Exit fullscreen mode

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'];
}



Enter fullscreen mode Exit fullscreen mode

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');
    }
};


Enter fullscreen mode Exit fullscreen mode

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);
    }
}


Enter fullscreen mode Exit fullscreen mode

Last, open the routes/api.php file and add the following at top of the file:



use App\Http\Controllers\PlayerController;
...


Enter fullscreen mode Exit fullscreen mode

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']);


Enter fullscreen mode Exit fullscreen mode

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']);


Enter fullscreen mode Exit fullscreen mode

🐳 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


Enter fullscreen mode Exit fullscreen mode

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: {}


Enter fullscreen mode Exit fullscreen mode

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. Replace francescoxx 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


Enter fullscreen mode Exit fullscreen mode

To check if it's running, you can use the following command:



docker compose logs


Enter fullscreen mode Exit fullscreen mode

and the



docker ps -a


Enter fullscreen mode Exit fullscreen mode

If the output is like the following one, you are good to go:

docker ps -a

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.

tableplus

🏗️ Build the project

To build the project, type:



docker compose build


Enter fullscreen mode Exit fullscreen mode

And the output should be something like that:

docker compose build

🏃‍♂️ Run the project

Now we can run the project.



docker compose up laravelapp


Enter fullscreen mode Exit fullscreen mode

And this should be the output:

docker compose up laravelapp

💽 Apply the migrations

Now we need to apply the migrations.



docker compose exec laravelapp php artisan migrate


Enter fullscreen mode Exit fullscreen mode

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"
}


Enter fullscreen mode Exit fullscreen mode

The output should be something like that:

create player

Let's create two more players, make a POST request to localhost:8000/api/player.



{
    "name": "bbb",
    "email": "bbb@mail"
}


Enter fullscreen mode Exit fullscreen mode


{
    "name": "ccc",
    "email": "ccc@mail"
}


Enter fullscreen mode Exit fullscreen mode

📝 Get all players

To get all players, make a GET request to localhost:8000/api/player.

The output should be something like that:

get all players

📝 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:

get a player

📝 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"
}


Enter fullscreen mode Exit fullscreen mode

The output should be something like that:

update a player

📝 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:

delete a players

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

TablePlus connection

Then click on the Connect button at the bottom-right.

As you can see, we have a players table with 2 records:

TablePlus players table


🏁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.

Francesco

Top comments (8)

Collapse
 
rafaelvilar profile image
rafael-vilar

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

Collapse
 
francescoxx profile image
Francesco Ciulla

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)

Collapse
 
ucok23 profile image
Ucok I. L

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

Collapse
 
francescoxx profile image
Francesco Ciulla

strange, try to clone the github repository (link in the video description)

Collapse
 
akpevwe11 profile image
Akpevwe11

Nice article

Collapse
 
francescoxx profile image
Francesco Ciulla

thanks

Collapse
 
mishobo profile image
Hussein Abdallah Mishobo

well done.. built the entire project with no errors

Collapse
 
francescoxx profile image
Francesco Ciulla

thank you!