DEV Community

Cover image for Criando API RESTful simples com PHP e CodeIgniter
Matheus Sesso
Matheus Sesso

Posted on

Criando API RESTful simples com PHP e CodeIgniter

Olá pessoal! Através desse simples artigo eu vou ensinar como criamos API RESTful básica, mas utilizando o nosso bom e velho PHP e a ajuda do maravilhoso framework CodeIgniter 4.

Então inspirado na Chuck Norris API, no péssimo humorista brasileiro Leo Lins, a fim de praticar e na completa zueira… 🤣

Resolvi criar uma API com piadas e humor negro brasileiro que está disponibilizada neste repositório no GitHub e pode ser utilizada através deste link:

https://darkhumor-api.ddns.net/

Sendo assim, vou explicar de forma simples e tentar ser o mais prático possível, como foi feito o básico deste esse projeto.

O que vamos construir?

  • API RESTful com 5 endpoints que funcionam de verdade
  • Arquitetura MVC bem organizada (sem gambiarras!)
  • Base de dados JSON com mais de 1000 piadas
  • Validações e tratamento de erros
  • Código limpo que você vai conseguir manter

O que você precisa saber antes

  • PHP básico (orientação a objetos ajuda)
  • Conceitos de API REST (GET, POST, JSON...)
  • CodeIgniter 4 (vou explicar tudo, mas é bom ter uma noção)
  • Um servidor local (Docker, XAMPP ou WAMP)

Como vamos organizar tudo

Olha, o CodeIgniter 4 já vem com uma estrutura bem definida. Vamos usar só o que precisamos:

darkhumor-api/
├── app/
│   ├── Controllers/
│   │   └── JokesController.php      # Aqui fica toda lógica da API
│   ├── Models/
│   │   └── JokesModel.php           # Aqui gerenciamos os dados
│   ├── Config/
│   │   ├── Routes.php               # Definimos as URLs da API
│   │   └── App.php                  # Configurações básicas
│   └── Libraries/
│       └── jokes.json               # Nossa "base de dados"
├── public/
│   └── index.php                    # Ponto de entrada da aplicação
└── .htaccess                        # Para URLs bonitas
Enter fullscreen mode Exit fullscreen mode

Por que essa estrutura?

  • Controllers: Recebem as requisições HTTP e devolvem respostas
  • Models: Fazem toda a manipulação dos dados
  • Config: Configurações da aplicação
  • Libraries: Arquivos auxiliares (nosso JSON fica aqui)

Planejamento da API

Ela vai possuir ter 5 endpoints principais:

Método Endpoint O que faz
GET /jokes/random Pega uma piada aleatória
GET /jokes/random?category=obesidade Piada aleatória de uma categoria
GET /jokes/categories Lista todas as categorias
GET /jokes/search?query=gordo Busca piadas por palavra
GET /jokes/42 Pega uma piada específica pelo ID

Como vai ser a resposta?

Todas as piadas vão retornar nesse formato JSON:

{
    "id": 1043,
    "url": "http://localhost/jokes/1043",
    "value": "Texto da piada aqui...",
    "theme": "categoria da piada"
}
Enter fullscreen mode Exit fullscreen mode

Por que esse formato?

  • id: Para identificar unicamente cada piada
  • url: Para acessar diretamente a piada
  • value: O texto da piada em si
  • theme: A categoria/tema da piada

1- Configuração Básica

Antes de mais nada, precisamos configurar o CodeIgniter. Abra o arquivo app/Config/App.php e ajuste essas configurações:

<?php
// app/Config/App.php

namespace Config;

use CodeIgniter\Config\BaseConfig;

class App extends BaseConfig
{
    // Sua URL local (ajuste conforme seu ambiente)
    public string $baseURL = 'http://localhost/';

    // Remove o index.php das URLs (deixa mais bonito)
    public string $indexPage = '';

    // Protocolo de URI
    public string $uriProtocol = 'REQUEST_URI';

    // Idioma padrão
    public string $defaultLocale = 'pt-BR';

    // Não negociar idioma automaticamente
    public bool $negotiateLocale = false;

    // Idiomas suportados
    public array $supportedLocales = ['pt-BR'];
}
Enter fullscreen mode Exit fullscreen mode

Por que essas configurações?

  • $baseURL: É a URL base da sua aplicação.
  • $indexPage = '': Remove o index.php das URLs. Ao invés de /index.php/jokes/random, fica só /jokes/random
  • $defaultLocale: Define português brasileiro como padrão

2 - Configurando as Rotas

Agora vamos definir as URLs da nossa API. Abra o arquivo app/Config/Routes.php:

<?php
// app/Config/Routes.php

use CodeIgniter\Router\RouteCollection;

/**
 * @var RouteCollection $routes
 */

// Rota principal (opcional, para uma página inicial)
$routes->get('/', 'JokesController::index');

// Agrupamos todas as rotas de piadas com o prefixo 'jokes'
$routes->group('jokes', function($routes) {
    $routes->get('random', 'JokesController::random');        // /jokes/random
    $routes->get('categories', 'JokesController::categories'); // /jokes/categories
    $routes->get('search', 'JokesController::search');        // /jokes/search
    $routes->get('(:num)', 'JokesController::show/$1');       // /jokes/42
});
Enter fullscreen mode Exit fullscreen mode

Vamos entender cada linha:

  1. $routes->get('/', 'JokesController::index'):

    • Quando alguém acessar a raiz do site, chama o método index do JokesController
  2. $routes->group('jokes', function($routes) { ... }):

    • Agrupa todas as rotas com o prefixo jokes
    • Assim não precisamos repetir /jokes em cada rota
  3. $routes->get('random', 'JokesController::random'):

    • URL: /jokes/random
    • Chama o método random do controller
  4. $routes->get('(:num)', 'JokesController::show/$1'):

    • (:num) captura apenas números
    • $1 passa esse número como parâmetro para o método show
    • Exemplo: /jokes/42 → chama show(42)

3 - Criando o Model

O Model é onde fica toda a lógica de manipulação dos dados. É ele que vai ler o JSON, buscar piadas, filtrar por categoria, etc.

Crie o arquivo app/Models/JokesModel.php:

<?php
// app/Models/JokesModel.php

namespace App\Models;

use CodeIgniter\Model;

class JokesModel extends Model
{
    // Aqui vamos guardar todas as piadas carregadas do JSON
    protected $jokesData;

    // E aqui as categorias extraídas
    protected $categories;

    public function __construct()
    {
        parent::__construct();
        // Assim que o model é criado, já carrega os dados
        $this->loadJokesData();
    }

    /**
     * Carrega os dados das piadas do arquivo JSON
     * Essa função roda uma vez quando o model é instanciado
     */
    private function loadJokesData()
    {
        // APPPATH é uma constante do CodeIgniter que aponta para a pasta app/
        $jsonPath = APPPATH . 'Libraries/jokes.json';

        // Sempre verificar se o arquivo existe antes de tentar ler
        if (!file_exists($jsonPath)) {
            throw new \Exception('Arquivo de piadas não encontrado em: ' . $jsonPath);
        }

        // Lê todo o conteúdo do arquivo JSON
        $jsonContent = file_get_contents($jsonPath);

        // Converte o JSON em array PHP
        $this->jokesData = json_decode($jsonContent, true);

        // Verifica se deu erro na conversão do JSON
        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new \Exception('Erro ao decodificar JSON: ' . json_last_error_msg());
        }

        // Extrai as categorias únicas das piadas
        $this->categories = $this->extractCategories();
    }

    /**
     * Pega uma piada aleatória
     * Método mais simples da nossa API
     */
    public function getRandomJoke()
    {
        $jokes = $this->jokesData['jokes'];

        // array_rand() pega um índice aleatório do array
        // Depois usamos esse índice para pegar a piada
        return $jokes[array_rand($jokes)];
    }

    /**
     * Busca uma piada específica pelo ID
     */
    public function getJokeById($id)
    {
        $jokes = $this->jokesData['jokes'];

        // Percorre todas as piadas procurando pelo ID
        foreach ($jokes as $joke) {
            if ($joke['id'] == $id) {
                return $joke; // Achou! Retorna a piada
            }
        }

        // Não achou nada? Retorna null
        return null;
    }

    /**
     * Pega piadas de uma categoria específica
     * Exemplo: getJokesByCategory('obesidade')
     */
    public function getJokesByCategory($category)
    {
        $jokes = $this->jokesData['jokes'];
        $categoryJokes = [];

        foreach ($jokes as $joke) {
            // stripos() faz busca case-insensitive
            // Procura se a categoria está no tema da piada
            if (stripos($joke['theme'], $category) !== false) {
                $categoryJokes[] = $joke;
            }
        }

        return $categoryJokes;
    }

    /**
     * Busca piadas por qualquer texto
     * Procura tanto no texto da piada quanto no tema
     */
    public function searchJokes($query)
    {
        $jokes = $this->jokesData['jokes'];
        $results = [];

        foreach ($jokes as $joke) {
            // Busca no texto da piada OU no tema
            $foundInJoke = stripos($joke['joke'], $query) !== false;
            $foundInTheme = stripos($joke['theme'], $query) !== false;

            if ($foundInJoke || $foundInTheme) {
                $results[] = $joke;
            }
        }

        return $results;
    }

    /**
     * Retorna todas as categorias disponíveis
     */
    public function getCategories()
    {
        return $this->categories;
    }

    /**
     * Extrai categorias únicas dos temas das piadas
     * Esse método é mais complexo porque alguns temas são compostos
     * Exemplo: "Obesidade e AIDS" vira duas categorias: "obesidade" e "aids"
     */
    private function extractCategories()
    {
        // array_column pega só a coluna 'theme' de todas as piadas
        $themes = array_column($this->jokesData['jokes'], 'theme');
        $categories = [];

        foreach ($themes as $theme) {
            // Alguns temas são compostos: "Obesidade e AIDS", "Deficiência e regionalismo"
            // Vamos separar por "e" ou vírgula
            $parts = preg_split('/\s+e\s+|,\s*/', $theme);

            foreach ($parts as $part) {
                // Limpa espaços e converte para minúsculo
                $category = trim(strtolower($part));

                // Só adiciona se não estiver vazio e não existir ainda
                if (!empty($category) && !in_array($category, $categories)) {
                    $categories[] = $category;
                }
            }
        }

        // Ordena alfabeticamente para ficar organizado
        sort($categories);
        return $categories;
    }

    /**
     * Método auxiliar para pegar estatísticas (opcional)
     */
    public function getStats()
    {
        return [
            'total_jokes' => count($this->jokesData['jokes']),
            'total_categories' => count($this->categories),
            'description' => $this->jokesData['description'] ?? 'API de Humor Negro'
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Por que fazer assim?

  1. Carregamento único: Os dados são carregados uma vez no __construct() e ficam na memória
  2. Tratamento de erros: Sempre verificamos se o arquivo existe e se o JSON é válido
  3. Busca inteligente: Usamos stripos() para busca case-insensitive
  4. Categorias dinâmicas: Extraímos automaticamente as categorias dos temas

4 - Criando o Controller - O Maestro da API

Agora vem a parte mais importante: o Controller! É ele que recebe as requisições HTTP, chama o Model e retorna as respostas em JSON.

Crie o arquivo app/Controllers/JokesController.php:

<?php
// app/Controllers/JokesController.php

namespace App\Controllers;

use CodeIgniter\RESTful\ResourceController;
use CodeIgniter\API\ResponseTrait;
use App\Models\JokesModel;

class JokesController extends ResourceController
{
    // Esse trait nos dá métodos prontos para responder JSON
    use ResponseTrait;

    protected $jokesModel;

    public function __construct()
    {
        // Instancia o model assim que o controller é criado
        $this->jokesModel = new JokesModel();
    }

    /**
     * Página inicial (opcional)
     * Você pode criar uma view aqui ou só retornar uma mensagem
     */
    public function index()
    {
        return $this->respond([
            'message' => 'Bem-vindo à Dark Humor API!',
            'endpoints' => [
                'GET /jokes/random' => 'Piada aleatória',
                'GET /jokes/categories' => 'Lista de categorias',
                'GET /jokes/search?query=termo' => 'Busca piadas',
                'GET /jokes/{id}' => 'Piada específica'
            ]
        ]);
    }

    /**
     * GET /jokes/random
     * GET /jokes/random?category=obesidade
     */
    public function random()
    {
        // Verifica se foi passada uma categoria
        $category = service('request')->getGet('category');

        if ($category) {
            // Se tem categoria, chama método específico
            return $this->getRandomByCategory($category);
        }

        // Senão, pega uma piada aleatória qualquer
        $randomJoke = $this->jokesModel->getRandomJoke();
        $response = $this->formatJokeResponse($randomJoke);

        return $this->respond($response);
    }

    /**
     * Método privado para pegar piada aleatória por categoria
     */
    private function getRandomByCategory($category)
    {
        // Busca todas as piadas da categoria
        $categoryJokes = $this->jokesModel->getJokesByCategory($category);

        // Se não achou nenhuma, retorna erro 404
        if (empty($categoryJokes)) {
            return $this->failNotFound('Categoria não encontrada: ' . $category);
        }

        // Pega uma aleatória das que achou
        $randomJoke = $categoryJokes[array_rand($categoryJokes)];
        $response = $this->formatJokeResponse($randomJoke);

        return $this->respond($response);
    }

    /**
     * GET /jokes/categories
     * Retorna todas as categorias disponíveis
     */
    public function categories()
    {
        $categories = $this->jokesModel->getCategories();
        return $this->respond($categories);
    }

    /**
     * GET /jokes/search?query=gordo
     * Busca piadas por texto
     */
    public function search()
    {
        // Pega o parâmetro 'query' da URL
        $query = service('request')->getGet('query');

        // Se não passou o parâmetro, retorna erro
        if (!$query) {
            return $this->fail('Parâmetro query é obrigatório', 400);
        }

        // Busca as piadas
        $jokes = $this->jokesModel->searchJokes($query);
        $results = [];

        // Formata cada piada encontrada
        foreach ($jokes as $joke) {
            $results[] = $this->formatJokeResponse($joke);
        }

        // Retorna com o total de resultados
        $response = [
            'total' => count($results),
            'result' => $results
        ];

        return $this->respond($response);
    }

    /**
     * GET /jokes/42
     * Retorna uma piada específica pelo ID
     */
    public function show($id = null)
    {
        // Verifica se foi passado um ID
        if (!$id) {
            return $this->fail('ID é obrigatório', 400);
        }

        // Busca a piada no model
        $joke = $this->jokesModel->getJokeById($id);

        // Se não achou, retorna 404
        if (!$joke) {
            return $this->failNotFound('Piada não encontrada com ID: ' . $id);
        }

        // Formata e retorna
        $response = $this->formatJokeResponse($joke);
        return $this->respond($response);
    }

    /**
     * Método auxiliar para formatar a resposta das piadas
     * Padroniza o formato de saída da API
     */
    private function formatJokeResponse($joke)
    {
        return [
            'id' => $joke['id'],
            'url' => base_url('jokes/' . $joke['id']),
            'value' => $joke['joke'],
            'theme' => $joke['theme']
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Vamos entender o que está acontecendo:

1. Herança e Traits

class JokesController extends ResourceController
{
    use ResponseTrait;
Enter fullscreen mode Exit fullscreen mode
  • ResourceController: Classe base do CodeIgniter para APIs REST
  • ResponseTrait: Nos dá métodos como respond(), fail(), failNotFound()

2. Método random()

$category = service('request')->getGet('category');
Enter fullscreen mode Exit fullscreen mode
  • service('request') pega a instância da requisição HTTP
  • getGet('category') busca o parâmetro category na URL
  • Se /jokes/random?category=obesidade, $category será "obesidade"

3. Tratamento de Erros

if (empty($categoryJokes)) {
    return $this->failNotFound('Categoria não encontrada');
}
Enter fullscreen mode Exit fullscreen mode
  • Sempre validamos se encontramos dados
  • failNotFound() retorna HTTP 404 automaticamente
  • fail() retorna HTTP 400 (Bad Request)

4. Formatação Consistente

private function formatJokeResponse($joke)
Enter fullscreen mode Exit fullscreen mode
  • Método privado que padroniza todas as respostas
  • Evita repetir código
  • Se precisar mudar o formato, muda só aqui

5 - Estrutura do JSON

Para este projeto, vamos usar um arquivo JSON como base de dados. É simples, rápido e funciona perfeitamente para uma API de piadas.

Crie o arquivo app/Libraries/jokes.json e a estrutura do json será semelhante a essa:

{
    "total_jokes": 1000,
    "description": "Coleção de piadas de humor negro em português",
    "jokes": [
        {
            "id": 1,
            "joke": "Sou gordo! Adoro comer e não gosto de fazer exercício. Como vou emagrecer? Pegando AIDS!",
            "theme": "Obesidade e AIDS"
        },
        {
            "id": 2,
            "joke": "Tem ser humano que não é 100% humano. O nordestino no avião? 72%.",
            "theme": "Preconceito regional"
        },
        {
            "id": 3,
            "joke": "Se você for no zoológico, os animais vão tirar fotos de você.",
            "theme": "Obesidade"
        }
        // ... continue adicionando suas piadas aqui
    ]
}
Enter fullscreen mode Exit fullscreen mode

Por que essa estrutura?

  1. total_jokes: Facilita estatísticas sem contar o array toda vez
  2. description: Metadados sobre a coleção
  3. jokes: Array principal com todas as piadas
  4. id: Identificador único e sequencial (1, 2, 3...)
  5. joke: O texto da piada em si
  6. theme: Categoria para filtros e buscas

O arquivo .json completo com todas as piadas está disponibilizado neste repositório do GitHub.


Testando Nossa API

Agora que tudo está pronto, vamos testar se nossa API está funcionando! Vou te mostrar várias formas de testar.

Crie um arquivo test.php para testar:

<?php
// Função auxiliar para testar endpoints
function testEndpoint($url) {
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $response = curl_exec($ch);
    curl_close($ch);

    return json_decode($response, true);
}

// Testa piada aleatória
$randomJoke = testEndpoint('http://localhost/jokes/random');
echo "Piada aleatória: " . $randomJoke['value'] . "\n";

// Testa categorias
$categories = testEndpoint('http://localhost/jokes/categories');
echo "Total de categorias: " . count($categories) . "\n";

// Testa busca
$searchResults = testEndpoint('http://localhost/jokes/search?query=gordo');
echo "Piadas encontradas: " . $searchResults['total'] . "\n";
?>
Enter fullscreen mode Exit fullscreen mode

Respostas Esperadas

Piada aleatória:

{
    "id": 1043,
    "url": "http://localhost/jokes/1043",
    "value": "Eu sou tão feio que meu espelho me deu um processo...",
    "theme": "Aparência"
}
Enter fullscreen mode Exit fullscreen mode

Busca por categoria:

{
    "id": 23,
    "url": "http://localhost/jokes/23",
    "value": "Gordo no ônibus: parece um dinossauro entrando.",
    "theme": "Obesidade"
}
Enter fullscreen mode Exit fullscreen mode

Lista de categorias:

[
    "aborto",
    "abuso",
    "acidente",
    "amizade",
    "animais",
    "aparência"
]
Enter fullscreen mode Exit fullscreen mode

Busca por texto:

{
    "total": 74,
    "result": [
        {
            "id": 1,
            "url": "http://localhost/jokes/1",
            "value": "Sou gordo! Adoro comer...",
            "theme": "Obesidade"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

Bom, é isso ai! Se você leu até aqui, espero que tenha te ajudado!

Criamos uma API RESTful completamente funcional utilizando as facilidades que CodeIgniter proporciona com sua estrutura MVC, criando uma ótima base para podermos explorar melhorias nas próximas implementações com recursos mais avançados, como:

  1. Cache: Implementar cache Redis para performance
  2. Paginação: Adicionar paginação na busca
  3. Rate Limiting: Limitar requisições por IP
  4. Logs: Implementar logs estruturados
  5. Validação: Validar parâmetros de entrada
  6. Autenticação JWT: Para APIs privadas
  7. Upload de piadas: Endpoint para adicionar piadas
  8. Favoritos: Sistema de piadas favoritas
  9. Estatísticas: Analytics de uso da API
  10. Webhooks: Notificações de novas piadas

Existe um projeto 100% open-source desta API e está disponibilizada neste repositório no GitHub e para utilização através deste link: https://darkhumor-api.ddns.net/


Links Recomendados


Tags: #PHP #CodeIgniter4 #API #REST #JSON #WebDevelopment #Backend #MVC

Top comments (0)