Roteamento

Nota: Quer entender mais sobre roteamento? Confira a página "why a framework?" para uma explicação mais detalhada.

O roteamento básico no Flight é feito ao combinar um padrão de URL com uma função de callback ou um array de uma classe e método.

Flight::route('/', function(){
    echo 'hello world!';
});

As rotas são combinadas na ordem em que são definidas. A primeira rota que combinar com uma solicitação será invocada.

Callbacks/Funções

O callback pode ser qualquer objeto que seja chamável. Então, você pode usar uma função regular:

function hello() {
    echo 'hello world!';
}

Flight::route('/', 'hello');

Classes

Você também pode usar um método estático de uma classe:

class Greeting {
    public static function hello() {
        echo 'hello world!';
    }
}

Flight::route('/', [ 'Greeting','hello' ]);

Ou criando um objeto primeiro e depois chamando o método:

// Greeting.php
class Greeting
{
    public function __construct() {
        $this->name = 'John Doe';
    }

    public function hello() {
        echo "Hello, {$this->name}!";
    }
}

// index.php
$greeting = new Greeting();

Flight::route('/', [ $greeting, 'hello' ]);
// Você também pode fazer isso sem criar o objeto primeiro
// Nota: Nenhum argumento será injetado no construtor
Flight::route('/', [ 'Greeting', 'hello' ]);
// Além disso, você pode usar esta sintaxe mais curta
Flight::route('/', 'Greeting->hello');
// ou
Flight::route('/', Greeting::class.'->hello');

Injeção de Dependência via DIC (Container de Injeção de Dependência)

Se você quiser usar injeção de dependência via um container (PSR-11, PHP-DI, Dice, etc.), o único tipo de rotas onde isso está disponível é ou criando diretamente o objeto você mesmo e usando o container para criar seu objeto, ou você pode usar strings para definir a classe e o método a chamar. Você pode ir para a página Injeção de Dependência para mais informações.

Aqui está um exemplo rápido:

use flight\database\PdoWrapper;

// Greeting.php
class Greeting
{
    protected PdoWrapper $pdoWrapper;
    public function __construct(PdoWrapper $pdoWrapper) {
        $this->pdoWrapper = $pdoWrapper;
    }

    public function hello(int $id) {
        // faça algo com $this->pdoWrapper
        $name = $this->pdoWrapper->fetchField("SELECT name FROM users WHERE id = ?", [ $id ]);
        echo "Hello, world! My name is {$name}!";
    }
}

// index.php

// Configure o container com os parâmetros que você precisa
// Veja a página de Injeção de Dependência para mais informações sobre PSR-11
$dice = new \Dice\Dice();

// Não esqueça de reatribuir a variável com '$dice = '!!!!!
$dice = $dice->addRule('flight\database\PdoWrapper', [
    'shared' => true,
    'constructParams' => [ 
        'mysql:host=localhost;dbname=test', 
        'root',
        'password'
    ]
]);

// Registre o manipulador do container
Flight::registerContainerHandler(function($class, $params) use ($dice) {
    return $dice->create($class, $params);
});

// Rotas como de costume
Flight::route('/hello/@id', [ 'Greeting', 'hello' ]);
// ou
Flight::route('/hello/@id', 'Greeting->hello');
// ou
Flight::route('/hello/@id', 'Greeting::hello');

Flight::start();

Roteamento por Método

Por padrão, os padrões de rota são combinados com todos os métodos de solicitação. Você pode responder a métodos específicos colocando um identificador antes da URL.

Flight::route('GET /', function () {
  echo 'I received a GET request.';
});

Flight::route('POST /', function () {
  echo 'I received a POST request.';
});

// Você não pode usar Flight::get() para rotas, pois isso é um método para obter variáveis, não para criar uma rota.
// Flight::post('/', function() { /* code */ });
// Flight::patch('/', function() { /* code */ });
// Flight::put('/', function() { /* code */ });
// Flight::delete('/', function() { /* code */ });

Você também pode mapear vários métodos para um único callback usando um delimitador |:

Flight::route('GET|POST /', function () {
  echo 'I received either a GET or a POST request.';
});

Além disso, você pode obter o objeto Router, que tem alguns métodos auxiliares para você usar:

$router = Flight::router();

// mapeia todos os métodos
$router->map('/', function() {
    echo 'hello world!';
});

// solicitação GET
$router->get('/users', function() {
    echo 'users';
});
// $router->post();
// $router->put();
// $router->delete();
// $router->patch();

Expressões Regulares

Você pode usar expressões regulares em suas rotas:

Flight::route('/user/[0-9]+', function () {
  // Isso irá combinar com /user/1234
});

Embora este método esteja disponível, é recomendado usar parâmetros nomeados ou parâmetros nomeados com expressões regulares, pois eles são mais legíveis e fáceis de manter.

Parâmetros Nomeados

Você pode especificar parâmetros nomeados em suas rotas que serão passados para sua função de callback. Isso é mais para a legibilidade da rota do que qualquer outra coisa. Por favor, veja a seção abaixo sobre ressalva importante.

Flight::route('/@name/@id', function (string $name, string $id) {
  echo "hello, $name ($id)!";
});

Você também pode incluir expressões regulares com seus parâmetros nomeados usando o delimitador ::

Flight::route('/@name/@id:[0-9]{3}', function (string $name, string $id) {
  // Isso irá combinar com /bob/123
  // Mas não irá combinar com /bob/12345
});

Nota: Combinar grupos de regex () com parâmetros posicionais não é suportado. :'(

Ressalva Importante

Embora no exemplo acima pareça que @name está diretamente ligado à variável $name, não está. A ordem dos parâmetros na função de callback é o que determina o que é passado para ela. Então, se você inverter a ordem dos parâmetros na função de callback, as variáveis também serão invertidas. Aqui está um exemplo:

Flight::route('/@name/@id', function (string $id, string $name) {
  echo "hello, $name ($id)!";
});

E se você acessar a URL seguinte: /bob/123, a saída seria hello, 123 (bob)!. Por favor, tenha cuidado ao configurar suas rotas e funções de callback.

Parâmetros Opcionais

Você pode especificar parâmetros nomeados que são opcionais para combinação ao envolver segmentos em parênteses.

Flight::route(
  '/blog(/@year(/@month(/@day)))',
  function(?string $year, ?string $month, ?string $day) {
    // Isso irá combinar com as seguintes URLs:
    // /blog/2012/12/10
    // /blog/2012/12
    // /blog/2012
    // /blog
  }
);

Quaisquer parâmetros opcionais que não forem combinados serão passados como NULL.

Coringas

A combinação é feita apenas em segmentos individuais de URL. Se você quiser combinar vários segmentos, pode usar o curinga *.

Flight::route('/blog/*', function () {
  // Isso irá combinar com /blog/2000/02/01
});

Para rotear todas as solicitações para um único callback, você pode fazer:

Flight::route('*', function () {
  // Faça algo
});

Passagem

Você pode passar a execução para a próxima rota que combina retornando true da sua função de callback.

Flight::route('/user/@name', function (string $name) {
  // Verifique alguma condição
  if ($name !== "Bob") {
    // Continue para a próxima rota
    return true;
  }
});

Flight::route('/user/*', function () {
  // Isso será chamado
});

Alias de Rotas

Você pode atribuir um alias a uma rota, para que a URL possa ser gerada dinamicamente mais tarde no seu código (como em um template, por exemplo).

Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');

// mais tarde no código em algum lugar
Flight::getUrl('user_view', [ 'id' => 5 ]); // irá retornar '/users/5'

Isso é especialmente útil se sua URL acontecer de mudar. No exemplo acima, digamos que users foi movido para /admin/users/@id em vez disso. Com aliasing no lugar, você não precisa mudar em nenhum lugar onde você referencia o alias, porque o alias agora retornará /admin/users/5 como no exemplo acima.

O alias de rota ainda funciona em grupos também:

Flight::group('/users', function() {
    Flight::route('/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
});

// mais tarde no código em algum lugar
Flight::getUrl('user_view', [ 'id' => 5 ]); // irá retornar '/users/5'

Informações de Rota

Se você quiser inspecionar as informações da rota que combina, há 2 maneiras de fazer isso. Você pode usar a propriedade executedRoute ou pode solicitar que o objeto de rota seja passado para seu callback passando true como o terceiro parâmetro no método de rota. O objeto de rota sempre será o último parâmetro passado para sua função de callback.

Flight::route('/', function(\flight\net\Route $route) {
  // Array de métodos HTTP combinados
  $route->methods;

  // Array de parâmetros nomeados
  $route->params;

  // Expressão regular de combinação
  $route->regex;

  // Contém o conteúdo de qualquer '*' usado no padrão de URL
  $route->splat;

  // Mostra o caminho da URL....se você realmente precisar
  $route->pattern;

  // Mostra o que middleware é atribuído a isso
  $route->middleware;

  // Mostra o alias atribuído a esta rota
  $route->alias;
}, true);

Ou se você quiser inspecionar a última rota executada, você pode fazer:

Flight::route('/', function() {
  $route = Flight::router()->executedRoute;
  // Faça algo com $route
  // Array de métodos HTTP combinados
  $route->methods;

  // Array de parâmetros nomeados
  $route->params;

  // Expressão regular de combinação
  $route->regex;

  // Contém o conteúdo de qualquer '*' usado no padrão de URL
  $route->splat;

  // Mostra o caminho da URL....se você realmente precisar
  $route->pattern;

  // Mostra o que middleware é atribuído a isso
  $route->middleware;

  // Mostra o alias atribuído a esta rota
  $route->alias;
});

Nota: A propriedade executedRoute só será definida após uma rota ter sido executada. Se você tentar acessá-la antes que uma rota tenha sido executada, ela será NULL. Você também pode usar executedRoute em middleware!

Agrupamento de Rotas

Pode haver momentos em que você deseja agrupar rotas relacionadas (como /api/v1). Você pode fazer isso usando o método group:

Flight::group('/api/v1', function () {
  Flight::route('/users', function () {
    // Combina com /api/v1/users
  });

  Flight::route('/posts', function () {
    // Combina com /api/v1/posts
  });
});

Você pode até aninhar grupos de grupos:

Flight::group('/api', function () {
  Flight::group('/v1', function () {
    // Flight::get() obtém variáveis, não define uma rota! Veja o contexto do objeto abaixo
    Flight::route('GET /users', function () {
      // Combina com GET /api/v1/users
    });

    Flight::post('/posts', function () {
      // Combina com POST /api/v1/posts
    });

    Flight::put('/posts/1', function () {
      // Combina com PUT /api/v1/posts
    });
  });
  Flight::group('/v2', function () {

    // Flight::get() obtém variáveis, não define uma rota! Veja o contexto do objeto abaixo
    Flight::route('GET /users', function () {
      // Combina com GET /api/v2/users
    });
  });
});

Agrupamento com Contexto de Objeto

Você ainda pode usar agrupamento de rotas com o objeto Engine da seguinte forma:

$app = new \flight\Engine();
$app->group('/api/v1', function (Router $router) {

  // use a variável $router
  $router->get('/users', function () {
    // Combina com GET /api/v1/users
  });

  $router->post('/posts', function () {
    // Combina com POST /api/v1/posts
  });
});

Agrupamento com Middleware

Você também pode atribuir middleware a um grupo de rotas:

Flight::group('/api/v1', function () {
  Flight::route('/users', function () {
    // Combina com /api/v1/users
  });
}, [ MyAuthMiddleware::class ]); // ou [ new MyAuthMiddleware() ] se você quiser usar uma instância

Veja mais detalhes na página group middleware.

Roteamento de Recursos

Você pode criar um conjunto de rotas para um recurso usando o método resource. Isso criará um conjunto de rotas para um recurso que segue as convenções RESTful.

Para criar um recurso, faça o seguinte:

Flight::resource('/users', UsersController::class);

E o que acontecerá em segundo plano é que ele criará as seguintes rotas:

[
      'index' => 'GET ',
      'create' => 'GET /create',
      'store' => 'POST ',
      'show' => 'GET /@id',
      'edit' => 'GET /@id/edit',
      'update' => 'PUT /@id',
      'destroy' => 'DELETE /@id'
]

E seu controlador ficará assim:

class UsersController
{
    public function index(): void
    {
    }

    public function show(string $id): void
    {
    }

    public function create(): void
    {
    }

    public function store(): void
    {
    }

    public function edit(string $id): void
    {
    }

    public function update(string $id): void
    {
    }

    public function destroy(string $id): void
    {
    }
}

Nota: Você pode visualizar as rotas recém-adicionadas com runway executando php runway routes.

Personalizando Rotas de Recursos

Há algumas opções para configurar as rotas de recursos.

Base de Alias

Você pode configurar o aliasBase. Por padrão, o alias é a última parte da URL especificada. Por exemplo, /users/ resultaria em um aliasBase de users. Quando essas rotas são criadas, os aliases são users.index, users.create, etc. Se você quiser mudar o alias, defina o aliasBase para o valor desejado.

Flight::resource('/users', UsersController::class, [ 'aliasBase' => 'user' ]);

Only e Except

Você também pode especificar quais rotas você quer criar usando as opções only e except.

Flight::resource('/users', UsersController::class, [ 'only' => [ 'index', 'show' ] ]);
Flight::resource('/users', UsersController::class, [ 'except' => [ 'create', 'store', 'edit', 'update', 'destroy' ] ]);

Essas são basicamente opções de lista branca e lista negra, para que você possa especificar quais rotas você quer criar.

Middleware

Você também pode especificar middleware para ser executado em cada uma das rotas criadas pelo método resource.

Flight::resource('/users', UsersController::class, [ 'middleware' => [ MyAuthMiddleware::class ] ]);

Streaming

Agora você pode transmitir respostas para o cliente usando o método streamWithHeaders(). Isso é útil para enviar arquivos grandes, processos de longa duração ou gerar respostas grandes. Transmitir uma rota é tratado de forma um pouco diferente de uma rota regular.

Nota: Respostas de streaming só estão disponíveis se você tiver flight.v2.output_buffering definido como false.

Stream com Cabeçalhos Manuais

Você pode transmitir uma resposta para o cliente usando o método stream() em uma rota. Se você fizer isso, você deve definir todos os métodos manualmente antes de enviar qualquer coisa para o cliente. Isso é feito com a função header() do PHP ou o método Flight::response()->setRealHeader().

Flight::route('/@filename', function($filename) {

    // obviamente você sanitizaria o caminho e o que seja.
    $fileNameSafe = basename($filename);

    // Se você tiver cabeçalhos adicionais para definir aqui após a rota ter sido executada
    // você deve defini-los antes que qualquer coisa seja ecoada.
    // Eles devem ser uma chamada bruta para a função header() ou 
    // uma chamada para Flight::response()->setRealHeader()
    header('Content-Disposition: attachment; filename="'.$fileNameSafe.'"');
    // ou
    Flight::response()->setRealHeader('Content-Disposition', 'attachment; filename="'.$fileNameSafe.'"');

    $filePath = '/some/path/to/files/'.$fileNameSafe;

    if (!is_readable($filePath)) {
        Flight::halt(404, 'File not found');
    }

    // defina manualmente o comprimento do conteúdo se você quiser
    header('Content-Length: '.filesize($filePath));

    // Transmita o arquivo para o cliente enquanto ele é lido
    readfile($filePath);

// Esta é a linha mágica aqui
})->stream();

Stream com Cabeçalhos

Você também pode usar o método streamWithHeaders() para definir os cabeçalhos antes de começar a transmitir.

Flight::route('/stream-users', function() {

    // você pode adicionar quaisquer cabeçalhos adicionais que quiser aqui
    // você só deve usar header() ou Flight::response()->setRealHeader()

    // no entanto, como você puxa seus dados, apenas como um exemplo...
    $users_stmt = Flight::db()->query("SELECT id, first_name, last_name FROM users");

    echo '{';
    $user_count = count($users);
    while($user = $users_stmt->fetch(PDO::FETCH_ASSOC)) {
        echo json_encode($user);
        if(--$user_count > 0) {
            echo ',';
        }

        // Isso é necessário para enviar os dados para o cliente
        ob_flush();
    }
    echo '}';

// Esta é como você definirá os cabeçalhos antes de começar a transmitir.
})->streamWithHeaders([
    'Content-Type' => 'application/json',
    'Content-Disposition' => 'attachment; filename="users.json"',
    // código de status opcional, padrão para 200
    'status' => 200
]);