Como criar API com PHP Puro?

Rafael Citario - Nov 2 - - Dev Community

Neste projeto vamos criar uma API Simple utilizando somente PHP em nenhum framework. Tudo o que vamos precisar será:

  • PHP - Essencial
  • Composer - Essencial
  • Editor/IDE como VScode ou PHPStorm
  • Docker - Preferencialmente, mas não essencial
  • Postman - Preferencialmente, mas não essencial

Vamos começar definindo nosso arquivo docker-compose.yml para a configuração de nosso banco de dados. Caso você não queira utilizar o docker para criação de banco de dados em containers uma solução é instalar o banco de dados em sua máquina. Para este tutorial vamos utilizar o MySQL.

Configuração

Após a criação da pasta onde nossa aplicação irá ficar, começamos configurando o docker-compose.yaml:

services:
  mysql:
    image: mysql:9.1.0
    ports:
      - '3306:3306'
    environment:
      MYSQL_ROOT_PASSWORD: useroot
      MYSQL_USER: api_user
      MYSQL_PASSWORD: api_password
      MYSQL_DATABASE: api_example
Enter fullscreen mode Exit fullscreen mode

Vamos quebrar este arquivo em partes para explicação:

services:
  mysql:
Enter fullscreen mode Exit fullscreen mode

Neste arquivo estamos definindo os serviços que serão utilizados.
Estou dando o nome de mysql para este serviço. Atenção o nome deste serviço será utilizado para a conexão com o banco dedados passando como host

image: mysql:9.1.0
Enter fullscreen mode Exit fullscreen mode

Em seguida estou definindo qual a imagem será utilizada para criação do nosso banco de dados, para este projeto estou utilizando a versão 9.1.0 do mysql.
Você pode encontrar esta e outras versões em Docker Hub.

ports:
      - '3306:3306'
Enter fullscreen mode Exit fullscreen mode

A porta está sendo setada como 3306. Está é a porta padrão do mysql!

Você pode notar que a porta está como 3306:3306, estes : significam que queremos escutar esta porta em nossa maquina local e não apenas no docker container, assim podendo acessa-la diretamente em nossa maquina.

environment:
      MYSQL_ROOT_PASSWORD: useroot
      MYSQL_USER: api_user
      MYSQL_PASSWORD: api_password
      MYSQL_DATABASE: api_example
Enter fullscreen mode Exit fullscreen mode

Como ambiente devemos definir as credenciais para nosso serviço mysql.
Sendo assim estamos definindo usuário, senha e nome do banco de dados utilizando as variáveis de ambiente:

MYSQL_USER: api_user // <--- Este é nosso usuário
Enter fullscreen mode Exit fullscreen mode
MYSQL_PASSWORD: api_password // <--- Este é nosso password
Enter fullscreen mode Exit fullscreen mode
MYSQL_DATABASE: api_example // <--- Este é nosso banco de dados
Enter fullscreen mode Exit fullscreen mode
MYSQL_ROOT_PASSWORD: useroot // <--- Está é a senha para o usuário root
Enter fullscreen mode Exit fullscreen mode

Para iniciarmos nosso container basta estar dentro da pasta onde se encontra o arquivo docker-compose.yaml e digitar em nosso terminal o seguinte comando:

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

Isto inicializará o serviço mysql em nosso container.
Caso você queira acessar o mysql dentro do terminal, pode utilizar este comando:

docker exec -it <nome do container> bash
Enter fullscreen mode Exit fullscreen mode

Após digitar este comando e apertar o enter você entrara no container onde a imagem mysql esta rodando.

O nome do container é formado por nomedapasta-nomedohost-número
Neste caso o nome do nosso container ficaria: create-api-php-mysql-1 se nossa aplicação fosse criada dentro do diretório "create-api-php".

Criaremos também um arquivo composer.json, este arquivo servira como base para instalação de bibliotecas externas que serão utilizadas no projeto. Neste projeto utilizaremos apenas a Dotenv.

{
  "require": {
      "vlucas/phpdotenv": "^2.4"
  },
  "autoload": {
      "psr-4": {
          "Src\\": "src/"
      }
  }
}
Enter fullscreen mode Exit fullscreen mode

Nesta linha estamos adicionando a biblioteca mais utilizada para dotenv no php.
Você pode encontrar o repositório para esta lib em: Github Repositório Vlucas

"require": {
      "vlucas/phpdotenv": "^2.4"
},
Enter fullscreen mode Exit fullscreen mode

Na linha abaixo estamos basicamente dizendo que utilizaremos o autoload com a configuração padrão da PSR-4. As PSR's mais utilizadas atualmente são PSR-12 e PSR-4, sendo a 12 mais utilizada até o momento. Más por hora vamos seguir utilizando a PSR-4.

"autoload": {
      "psr-4": {
          "Src\\": "src/"
      }
  }
Enter fullscreen mode Exit fullscreen mode

Com estes dois arquivos criados podemos dar o comando

composer install
Enter fullscreen mode Exit fullscreen mode

Ele instalará a biblioteca do Dotenv e definira as configurações para a PSR desejada.
Após este comando será criado em nosso ambiente o arquivo composer.lock.

Para quem vem do mundo JavaScript estes arquivos podem ser comparados com o package.json e package-lock.json.

Você verá também que foi adiciona uma pasta em seu diretório com o nome vendor nela está localizada nossa lib Dotenv e também um arquivo muito importante: autoload.php.
Neste arquivo não precisamos mexer ou alterar qualquer coisa, pois ele será o responsável por transacionar as informações do Dotenv entre nossos demais arquivos.

Criaremos também um arquivo chamado bootstrap.php este arquivo é responsável por inicializar nossa aplicação e conectar alguns parâmetros importantes para que tudo funcione de forma esperada:

<?php
require 'vendor/autoload.php';

use Dotenv\Dotenv;

$dotenv = new DotEnv(__DIR__);
$dotenv->load();

Enter fullscreen mode Exit fullscreen mode

Podemos criar então o arquivo .env para adicionarmos as variáveis de ambiente que serão responsáveis por conectar com nosso banco de dados mysql.
Adicionamos então:

DB_HOST='127.0.0.1'
DB_PORT='3306'
DB_DATABASE='api_example '
DB_USERNAME='api_user'
DB_PASSWORD='api_password'
Enter fullscreen mode Exit fullscreen mode

Criaremos também um arquivo .env.example onde será guardada uma cópia dessas informações para caso alguém queira clonar nosso repositório ou até mesmo para nós do futuro caso queiramos dar continuidade ao nosso projeto, assim teremos as informações necessárias para saber o que precisar definir e o que não precisamos.

DB_HOST='127.0.0.1'
DB_PORT='3306'
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=
Enter fullscreen mode Exit fullscreen mode

O motivo para qual criaremos estes dois arquivos, um contendo toda a informação e outro contendo somente uma parte da informação é por que o arquivo .env não deve subir ao repositório, pois contem informações confidenciais. Digamos que futuramente queiramos utilizar uma API de terceiros onde precisamos adicionar um token para o acesso então guardaremos esta informação dentro do arquivo .env.

Para impedir que o arquivo .env suba para nosso repositório criaremos um arquivo chamado .gitignore e adicionaremos as seguintes informações:

vendor/
.env
Enter fullscreen mode Exit fullscreen mode

Assim definimos que o arquivo .env e todo o conteúdo da pasta vendor não serão comitados.

Com isso finalizamos a configuração do nosso projeto e estamos livres para seguir com a codificação.

Codificação

Criaremos os seguintes diretorios src/System e dentro de System o arquivo DatabaseConnector.php

<?php

namespace Src\System;

class DatabaseConnector {

  private $dbConnection = null;

  public function __construct() {
    $host = getenv('DB_HOST');
    $port = getenv('DB_PORT');
    $db   = getenv('DB_DATABASE');
    $user = getenv('DB_USERNAME');
    $pass = getenv('DB_PASSWORD');

    try {
      $this->dbConnection = new \PDO(
        "mysql:host=$host;port=$port;charset=utf8mb4;dbname=$db",
        $user,
        $pass
      );
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }
  }

  public function getConnection() {
    return $this->dbConnection;
  }
}
Enter fullscreen mode Exit fullscreen mode

Aqui estamos definindo um namespace para este arquivo, para que possamos utilizá-lo futuramente dentro de outros arquivos.

namespace Src\System;
Enter fullscreen mode Exit fullscreen mode

Criaremos nossa classe com o mesmo nome do arquivo e criaremos uma variável privada como nome $dbConnection passando o valor null.
Esta variável será responsável por uma nova instancia desta classe e nos conectar com o banco de dados.
Veremos mais a seguir quando implementarmos o try-catch.

class DatabaseConnector {

  private $dbConnection = null;

  public function __construct() {
    $host = getenv('DB_HOST');
    $port = getenv('DB_PORT');
    $db   = getenv('DB_DATABASE');
    $user = getenv('DB_USERNAME');
    $pass = getenv('DB_PASSWORD');

    try {
      $this->dbConnection = new \PDO(
        "mysql:host=$host;port=$port;charset=utf8mb4;dbname=$db",
        $user,
        $pass
      );
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Dentro do constructor criaremos as seguintes variáveis e aferindo os valores capturados do arquivo .env com Dotenv.

$host = getenv('DB_HOST');
$port = getenv('DB_PORT');
$db   = getenv('DB_DATABASE');
$user = getenv('DB_USERNAME');
$pass = getenv('DB_PASSWORD');
Enter fullscreen mode Exit fullscreen mode

Ainda dentro do constructor faremos um try-catch para validação da ação que queremos realizar:

try {
      $this->dbConnection = new \PDO(
        "mysql:host=$host;port=$port;charset=utf8mb4;dbname=$db",
        $user,
        $pass
      );
    }
Enter fullscreen mode Exit fullscreen mode

Dentro deste try estamos tentando criar uma nova instancia da nossa classe e passando para dentro da variável $dbConnection. Estamos utilizando um módulo PDO para isso onde recebe os parâmetros

  • DSN - Data Source Name ou URI
  • - mysql: Sendo o serviço/banco que estamos utilizando.
  • - host=$host; Nosso Host
  • - port=$port; Nossa porta
  • - charset=utf8mb4; Definição do charset utf8 para o banco de dados
  • - dbname=$db O Nome de nosso banco de dados
  • USER - Usuário para login no banco de dados
  • PASS - Senha para login no banco de dados

Caso de erro:

 catch (\PDOException $e) {
      exit($e->getMessage());
    }
Enter fullscreen mode Exit fullscreen mode

Acionaremos uma exception propria do PDO e retornamos a mensagem de erro.
Claramente este é somente um exemplo de como devemos apresentar os erros em ambiente de desenvolvimento. Para ambiente de produção é uma boa prática apresentarmos erros mais concisos que nos ajudem a entender de forma mais clara o problema ocorrido.

Já fora do constructor mas dentro de nossa classe, criaremos a seguinte função:

  public function getConnection() {
    return $this->dbConnection;
  }
Enter fullscreen mode Exit fullscreen mode

Sendo responsável por chamar nossa variável contendo a instancia de nossa conexão.

Lembra do nosso arquivo bootstrap.php? Vamos adicionar as seguintes linhas de código nele:

use Src\System\DatabaseConnector;
$dbConnection = (new DatabaseConnector())->getConnection();
Enter fullscreen mode Exit fullscreen mode

Ficando desta forma:

<?php
require 'vendor/autoload.php';

use Dotenv\Dotenv;

use Src\System\DatabaseConnector;

$dotenv = new DotEnv(__DIR__);
$dotenv->load();

$dbConnection = (new DatabaseConnector())->getConnection();
Enter fullscreen mode Exit fullscreen mode

Dentro da pasta src criaremos mais um diretório com o nome de Database e dentro dele o arquivo database_seed.php.
Este arquivo será responsável por popular nossa base de dados pela primeira vez, assim caso queiramos compartilhar este projeto com algum ele não vira com uma base de dados vazia.

Dentro deste arquivo adicionaremos os seguintes códigos:

<?php
require 'bootstrap.php';

$statement = <<<EOS
  DROP TABLE IF EXISTS person;
  CREATE TABLE IF NOT EXISTS person (
      id INT NOT NULL AUTO_INCREMENT,
      firstname VARCHAR(100) NOT NULL,
      lastname VARCHAR(100) NOT NULL,
      firstparent_id INT DEFAULT NULL,
      secondparent_id INT DEFAULT NULL,
      PRIMARY KEY (id),
      FOREIGN KEY (firstparent_id)
          REFERENCES person(id)
          ON DELETE SET NULL,
      FOREIGN KEY (secondparent_id)
          REFERENCES person(id)
          ON DELETE SET NULL
  ) ENGINE=INNODB;

  INSERT INTO person
      (id, firstname, lastname, firstparent_id, secondparent_id)
  VALUES
      (1, 'Krasimir', 'Hristozov', null, null),
      (2, 'Maria', 'Hristozova', null, null),
      (3, 'Masha', 'Hristozova', 1, 2),
      (4, 'Jane', 'Smith', null, null),
      (5, 'John', 'Smith', null, null),
      (6, 'Richard', 'Smith', 4, 5),
      (7, 'Donna', 'Smith', 4, 5),
      (8, 'Josh', 'Harrelson', null, null),
      (9, 'Anna', 'Harrelson', 7, 8);
EOS;

try {
  $createTable = $dbConnection->exec($statement);
  echo "Success!\n";
} catch (\PDOException $e) {
  exit($e->getMessage());
}
Enter fullscreen mode Exit fullscreen mode

Importamos require 'bootstrap.php'; pois dentro de nosso arquivo bootstrap já realizamos a importação da variável que é responsável por instanciar nosso banco de dados.

<?php
require 'bootstrap.php';
Enter fullscreen mode Exit fullscreen mode

Criamos uma variável com o nome de $statement que tem como valor um Heredoc

$statement = <<<EOS
EOL;
Enter fullscreen mode Exit fullscreen mode

Dentro deste Heredoc adicionaremos algumas querys:

DROP TABLE IF EXISTS person;
  CREATE TABLE IF NOT EXISTS person (
      id INT NOT NULL AUTO_INCREMENT,
      firstname VARCHAR(100) NOT NULL,
      lastname VARCHAR(100) NOT NULL,
      firstparent_id INT DEFAULT NULL,
      secondparent_id INT DEFAULT NULL,
      PRIMARY KEY (id),
      FOREIGN KEY (firstparent_id)
          REFERENCES person(id)
          ON DELETE SET NULL,
      FOREIGN KEY (secondparent_id)
          REFERENCES person(id)
          ON DELETE SET NULL
  ) ENGINE=INNODB;
Enter fullscreen mode Exit fullscreen mode

Aqui estou optando por drop table para derrubar toda a base e logo em seguida iniciar uma nova, entretanto se você quiser pode retirar esta linha de código.

A seguinte linha de código especifica que esta tabela será utilizada para fazer transações e tera conexção entre as tabelas. Caso queira aprender mais sobre esta declaração mysql: documentação innoDb

ENGINE=INNODB;
Enter fullscreen mode Exit fullscreen mode

Dentro do mesmo Heredoc adicionaremos outra query:

INSERT INTO person
      (id, firstname, lastname, firstparent_id, secondparent_id)
  VALUES
      (1, 'Krasimir', 'Hristozov', null, null),
      (2, 'Maria', 'Hristozova', null, null),
      (3, 'Masha', 'Hristozova', 1, 2),
      (4, 'Jane', 'Smith', null, null),
      (5, 'John', 'Smith', null, null),
      (6, 'Richard', 'Smith', 4, 5),
      (7, 'Donna', 'Smith', 4, 5),
      (8, 'Josh', 'Harrelson', null, null),
      (9, 'Anna', 'Harrelson', 7, 8);
Enter fullscreen mode Exit fullscreen mode

Aqui estamos inserindo alguns dados dentro da tabela person.

Criamos um try-catch ao final do arquivo onde tentamos inicializar as querys e caso de erro retornamos uma mensagem de erro assim como fizemos nos tratamentos de dados nos códigos acima.

try {
  $createTable = $dbConnection->exec($statement);
  echo "Success!\n";
} catch (\PDOException $e) {
  exit($e->getMessage());
}
Enter fullscreen mode Exit fullscreen mode

Dentro de src criaremos outro diretório com o nome de TableGateways e dentro dela criaremos o arquivo: PersonGateway.php.

<?php

namespace Src\TableGateways;

class PersonGateway {
  private $db = null;
  public function __construct($db) {
    $this->db = $db;
  }

  public function findAll() {
    $statement = "
      SELECT 
        id, firstname, lastname, firstparent_id, secondparent_id
      FROM
        person;
    ";

    try {
      $statement = $this->db->query($statement);
      $result    = $statement->fetchAll(\PDO::FETCH_ASSOC);
      return $result;
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }
  }

  public function find($id) {
    $statement = "
      SELECT
        id, firstname, lastname, firstparent_id, secondparent_id
      FROM
        person
      WHERE
        id LIKE ?;
    ";

    try {
      $statement = $this->db->prepare($statement);
      $statement->execute(array($id));
      $result = $statement->fetchAll(\PDO::FETCH_ASSOC);
      return $result;
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }
  }

  public function insert(array $input) {
    $statement = "
      INSERT INTO person 
        (firstname, lastname, firstparent_id, secondparent_id)
      VALUES
        (:firstname, :lastname, :firstparent_id, :secondparent_id);
    ";

    try {
      $statement = $this->db->prepare($statement);
      $statement->execute(array(
        'firstname' => $input['firstname'],
        'lastname' => $input['lastname'],
        'firstparent_id' => $input['fristparent_id'] ?? null,
        'secondparent_id' => $input['secondparent_id'] ?? null,
      ));

      return $statement->rowCount();
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }
  }

  public function update($id, array $input) {
    $statement = "
      UPDATE person
      SET
        firstname = :firstname,
        lastname = :lastname,
        firstparent_id = :firstparent_id,
        secondparent_id = :secondparent_id
      WHERE id = :id;
    ";

    try {
      $statement = $this->db->prepare($statement);
      $statement->execute(array(
        'id' => (int) $id,
        'firstname' => $input['firstname'],
        'lastname' => $input['lastname'],
        'firstparent_id' => $input['firstparent_id'] ?? null,
        'secondparent_id' => $input['secondparent_id'] ?? null,
      ));

      return $statement->rowCount();
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }
  }

  public function delete($id) {
    $statement = "
      DELETE FROM person
      WHERE id = :id
    ";

    try {
      $statement = $this->db->prepare($statement);
      $statement->execute(array('id' => $id));
      return $statement->rowCount();
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Os arquivos dentro desta pasta seram responsáveis por interagir com nosso banco de dados, quase como um Repository.

Em nossa classe PersonGateway adicionaremos o seguinte contructor:

 private $db = null;
  public function __construct($db) {
    $this->db = $db;
  }
Enter fullscreen mode Exit fullscreen mode

Adicionaremos este constructor pois nossa classe será chamada em outros arquivos para que possamos acionar alguns methodos de nossa classe.

Veja os metodos a seguir:

Metodo responsável por listar todos os usuários de nossa tabela

public function findAll() {
    $statement = "
      SELECT 
        id, firstname, lastname, firstparent_id, secondparent_id
      FROM
        person;
    ";

    try {
      $statement = $this->db->query($statement);
      $result    = $statement->fetchAll(\PDO::FETCH_ASSOC);
      return $result;
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }
  }
Enter fullscreen mode Exit fullscreen mode

Metodo responsável por listar um único usuário de nossa tabela

  public function find($id) {
    $statement = "
      SELECT
        id, firstname, lastname, firstparent_id, secondparent_id
      FROM
        person
      WHERE
        id LIKE ?;
    ";

    try {
      $statement = $this->db->prepare($statement);
      $statement->execute(array($id));
      $result = $statement->fetchAll(\PDO::FETCH_ASSOC);
      return $result;
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }
  }
Enter fullscreen mode Exit fullscreen mode

Metodo responsável inserir um usuário em nossa tabela

  public function insert(array $input) {
    $statement = "
      INSERT INTO person 
        (firstname, lastname, firstparent_id, secondparent_id)
      VALUES
        (:firstname, :lastname, :firstparent_id, :secondparent_id);
    ";

    try {
      $statement = $this->db->prepare($statement);
      $statement->execute(array(
        'firstname' => $input['firstname'],
        'lastname' => $input['lastname'],
        'firstparent_id' => $input['fristparent_id'] ?? null,
        'secondparent_id' => $input['secondparent_id'] ?? null,
      ));

      return $statement->rowCount();
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }
  }
Enter fullscreen mode Exit fullscreen mode

Metodo responsável por atualizar as informações de um usuário em nossa tabela

  public function update($id, array $input) {
    $statement = "
      UPDATE person
      SET
        firstname = :firstname,
        lastname = :lastname,
        firstparent_id = :firstparent_id,
        secondparent_id = :secondparent_id
      WHERE id = :id;
    ";

    try {
      $statement = $this->db->prepare($statement);
      $statement->execute(array(
        'id' => (int) $id,
        'firstname' => $input['firstname'],
        'lastname' => $input['lastname'],
        'firstparent_id' => $input['firstparent_id'] ?? null,
        'secondparent_id' => $input['secondparent_id'] ?? null,
      ));

      return $statement->rowCount();
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }
  }
Enter fullscreen mode Exit fullscreen mode

Metodo responsável por deletar um usuário de nossa tabela

  public function delete($id) {
    $statement = "
      DELETE FROM person
      WHERE id = :id
    ";

    try {
      $statement = $this->db->prepare($statement);
      $statement->execute(array('id' => $id));
      return $statement->rowCount();
    } catch (\PDOException $e) {
      exit($e->getMessage());
    }
  }
Enter fullscreen mode Exit fullscreen mode

Criaremos dentro de src um diretório com o nome de Controller e dentro ele o arquivo: PersonController.php.
Os arquivos dentro deste diretório são responsáveis por interagir com a rota de nossa aplicação. Aqui interagirmos diretamente com o banco, mas poderíamos utilizar uma camada de serviços e limitar toda logica e regra de negócios para esta camada.
Caso deseje criar a camada de serviços seria desta forma:

src/
├── Controller/
├── Database/
├── System/
├── TableGateways/
└── Services/
Enter fullscreen mode Exit fullscreen mode

Entretanto nosso intuito não é se aprofundar neste tipo de arquitetura, por hora vamos seguir com o arquivo controller:

<?php

namespace Src\Controller;

use Src\TableGateways\PersonGateway;

class PersonController {
  private $db;
  private $requestMethod;
  private $userId;
  private $personGateway;
  public function __construct($db, $requestMethod, $userId) {
    $this->db            = $db;
    $this->requestMethod = $requestMethod;
    $this->userId        = $userId;

    $this->personGateway = new PersonGateway($this->db);
  }

  public function processRequest() {
    switch ($this->requestMethod) {
      case 'GET':
        if ($this->userId) {
          $response = $this->getUser($this->userId);
        } else {
          $response = $this->getAllUsers();
        }
        break;

      case 'POST':
        $response = $this->createUserFromRequest();
        break;
      case 'PUT':
        $response = $this->updateUserFromRequest($this->userId);
        break;
      case 'DELETE':
        $response = $this->deleteUser($this->userId);
        break;
      default:
        $response = $this->notFoundResponse();
        break;
    }

    header($response['status_code_header']);
    if ($response['body']) {
      echo $response['body'];
    }
  }

  private function getUser($id): mixed {
    $result = $this->personGateway->find($id);
    if (!$result) {
      return $this->notFoundResponse();
    }
    $response['status_code_header'] = 'HTTP/1.1 200 OK';
    $response['body']               = json_encode($result);
    return $response;
  }

  private function getAllUsers(): mixed {
    $result = $this->personGateway->findAll();
    $response['status_code_header'] = 'HTTP/1.1 200 OK';
    $response['body']                 = json_encode($result);
    return $response;
  }

  private function createUserFromRequest(): mixed {
    $input = (array) json_decode(file_get_contents('php://input'), TRUE);
    if (!$this->validatePerson($input)) {
      return $this->unprocessableEntityResponse();
    }
    $this->personGateway->insert($input);
    $response['status_code_header'] = 'HTTP/1.1 201 Created';
    $response['body']               = null;
    return $response;
  }

  private function updateUserFromRequest($id): mixed {
    $result = $this->personGateway->find($id);
    if (!$result) {
      return $this->notFoundResponse();
    }
    $input = (array) json_decode(file_get_contents('php://input'), TRUE);
    if (! $this->validatePerson($input)) {
      return $this->unprocessableEntityResponse();
    }
    $this->personGateway->update($id, $input);
    $response['status_code_header'] = 'HTTP/1.1 200 OK';
    $response['body']               = null;
    return $response;
  }

  private function deleteUser($id): mixed {
    $result = $this->personGateway->find($id);
    if (! $result) {
      $this->notFoundResponse();
    }
    $this->personGateway->delete($id);
    $response['status_code_header'] = 'HTTP/1.1 200 OK';
    $response['body']                = null;
    return $response;
  }

  private function validatePerson(array $input): bool {
    if (!isset($input['firstname'])) {
      return false;
    }

    if (! isset($input['lastname'])) {
      return false;
    }
    return true;
  }

  private function unprocessableEntityResponse(): array {
    $response['status_code_header'] = 'HTTP/1.1 402 Unprocessable Entity';
    $response['body']               = json_encode([
      'error' => 'Invalid Input',
    ]);

    return $response;
  }

  private function notFoundResponse(): array {
    $response['status_code_header'] = 'HTTP/1.1 404 Not Found';
    $response['body']                 = null;
    return $response;
  }
}
Enter fullscreen mode Exit fullscreen mode

Dentro de nossa classe PersonController adicionaremos:

private $db;
  private $requestMethod;
  private $userId;
  private $personGateway;
  public function __construct($db, $requestMethod, $userId) {
    $this->db            = $db;
    $this->requestMethod = $requestMethod;
    $this->userId        = $userId;

    $this->personGateway = new PersonGateway($this->db);
  }
Enter fullscreen mode Exit fullscreen mode

Assim garantimos que estamos interagindo com uma nova instancia de nosso banco de dados.

Criamos também um método para processar nossas requisições:

public function processRequest() {
    switch ($this->requestMethod) {
      case 'GET':
        if ($this->userId) {
          $response = $this->getUser($this->userId);
        } else {
          $response = $this->getAllUsers();
        }
        break;

      case 'POST':
        $response = $this->createUserFromRequest();
        break;
      case 'PUT':
        $response = $this->updateUserFromRequest($this->userId);
        break;
      case 'DELETE':
        $response = $this->deleteUser($this->userId);
        break;
      default:
        $response = $this->notFoundResponse();
        break;
    }

header($response['status_code_header']);
    if ($response['body']) {
      echo $response['body'];
    }
}
Enter fullscreen mode Exit fullscreen mode

Este header é responsável por transmitir o status code e caso um body seja criado ele retorna este mesmo body para que seja visualizado.

header($response['status_code_header']);
    if ($response['body']) {
      echo $response['body'];
    }
Enter fullscreen mode Exit fullscreen mode

Criamos também os métodos que iram interagir com as rotas:

Metodo responsável por interagir com a rota de listagem do usuário

private function getUser($id): mixed {
    $result = $this->personGateway->find($id);
    if (!$result) {
      return $this->notFoundResponse();
    }
    $response['status_code_header'] = 'HTTP/1.1 200 OK';
    $response['body']               = json_encode($result);
    return $response;
  }

  private function getAllUsers(): mixed {
    $result = $this->personGateway->findAll();
    $response['status_code_header'] = 'HTTP/1.1 200 OK';
    $response['body']                 = json_encode($result);
    return $response;
  }
Enter fullscreen mode Exit fullscreen mode

Metodo responsável por interagir com a rota de criação do usuário

  private function createUserFromRequest(): mixed {
    $input = (array) json_decode(file_get_contents('php://input'), TRUE);
    if (!$this->validatePerson($input)) {
      return $this->unprocessableEntityResponse();
    }
    $this->personGateway->insert($input);
    $response['status_code_header'] = 'HTTP/1.1 201 Created';
    $response['body']               = null;
    return $response;
  }
Enter fullscreen mode Exit fullscreen mode

Metodo responsável por interagir com a rota de update do usuário

  private function updateUserFromRequest($id): mixed {
    $result = $this->personGateway->find($id);
    if (!$result) {
      return $this->notFoundResponse();
    }
    $input = (array) json_decode(file_get_contents('php://input'), TRUE);
    if (! $this->validatePerson($input)) {
      return $this->unprocessableEntityResponse();
    }
    $this->personGateway->update($id, $input);
    $response['status_code_header'] = 'HTTP/1.1 200 OK';
    $response['body']               = null;
    return $response;
  }
Enter fullscreen mode Exit fullscreen mode

Metodo responsável por interagir com a rota de deleção do usuário

  private function deleteUser($id): mixed {
    $result = $this->personGateway->find($id);
    if (! $result) {
      $this->notFoundResponse();
    }
    $this->personGateway->delete($id);
    $response['status_code_header'] = 'HTTP/1.1 200 OK';
    $response['body']                = null;
    return $response;
  }
Enter fullscreen mode Exit fullscreen mode

Métodos responsáveis por validação

  private function validatePerson(array $input): bool {
    if (!isset($input['firstname'])) {
      return false;
    }

    if (! isset($input['lastname'])) {
      return false;
    }
    return true;
  }
Enter fullscreen mode Exit fullscreen mode
private function unprocessableEntityResponse(): array {
    $response['status_code_header'] = 'HTTP/1.1 402 Unprocessable Entity';
    $response['body']               = json_encode([
      'error' => 'Invalid Input',
    ]);

    return $response;
  }
Enter fullscreen mode Exit fullscreen mode
  private function notFoundResponse(): array {
    $response['status_code_header'] = 'HTTP/1.1 404 Not Found';
    $response['body']                 = null;
    return $response;
  }
Enter fullscreen mode Exit fullscreen mode

Por final criaremos um diretório fora de nossa pasta src com o nome de Public.
Esta pasta é responsável por conter o arquivo de exibição do php.
Criaremos dentro dela o arquivo: index.php
Adicionaremos o seguinte código:

<?php
require "../bootstrap.php";

use Src\Controller\PersonController;

header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: OPTIONS,GET,POST,PUT,DELETE");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");

$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$uri = explode('/', $uri);

if ($uri[1] !== 'person') {
  header("HTTP/1.1 404 Not Found");
  exit();
}

$userId = null;
if (isset($uri[2])) {
  $userId = (int) $uri[2];
}

$requestMethod = $_SERVER['REQUEST_METHOD'];

$controller = new PersonController($dbConnection, $requestMethod, $userId);
$controller->processRequest();
Enter fullscreen mode Exit fullscreen mode

Este arquivo está sendo responsável por setar os headeres e verificar o acesso à url. Caso o acesso seja bem sucedido ele retornar os conteúdos, caso não seja ele retorna um erro.

Para acessar sua aplicação basta subir um servidor utilizando o servidor interno do PHP:

php -S localhost:8000 -t public
Enter fullscreen mode Exit fullscreen mode

se não tiver inicializado o container digite o seguinte comando no terminal:

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

Agora basta utilizar o postman ou qualquer outra aplicação que te ajude a interagir com url.

Minhas redes sociais:
Github Linkedin

. .