Aplicação desenvolvida com PHP e Angular, porém, com o foco direcionado ao app em PHP que foi criado baseado nos princípios SOLID e na arquitetura package-by-feature, garantindo maior legibilidade e organização do código, bem como a implementação de testes automatizados.
Data de criação: Jun 9, 2024
A aplicação com PHP adota princípios de design de software, como os princípios SOLID, e uma arquitetura modular baseada em funcionalidades (package-by-feature). Essa abordagem garante que o código seja bem estruturado, fácil de entender e manter, além de permitir uma escalabilidade e extensibilidade mais simples.
Note
O upload das imagens é relativamente simples, com validação e salvas no formato BLOB (Binary Large Object), num cenário real, seriam salvas em uma CDN (Content Delivery Network).
- 📁 Padrão Package By Feature
- 🙍 User
- Criar usuário
- Autenticação - JWT
- Informações do Usuário
- Editar Usuário
- 📚 Book
- Criar livro
- Editar livro
- Informações de um livro
- Todos os livros
- Remover um livro
- 🙍 User
- ⚡ Dependencies:
- phpunit/phpunit:
^10.5
- vlucas/phpdotenv:
^5.6
- @angular/cli:
^17.3.8
- primeng:
^17.18.1
, - and more...
- phpunit/phpunit:
- 📡 DevOps
- NGINX
- Docker
Note
Para adicionar uma nova rota, deve-se levar em consideração os use cases, controllers e as factories.
|-- routes
| |-- api.php
|-- config
| |-- factories.php
|-- UseCases
| |-- Intro
| | |-- WelcomeMessage
| | | |-- WelcomeMessageController.php
| | | |-- WelcomeMessageFactory.php
| | | |-- IWelcomeMessageUseCase.php
| | | |-- WelcomeMessageUseCase.php
Em routes/api.php
, referencie o controller através do namespace. Porém, deve-se remover o App\UseCases\
, mantendo apenas o restante, nesse caso, Intro\WelcomeMessage\WelcomeMessageController
.
Exemplo:
<?php
use App\Http\Router;
Route::get('/', 'Intro\WelcomeMessage\WelcomeMessageController');
Agora, em config/factories.php
, associe o controller passado na rota com a factory do use case.
Note
A factory
é responsável por criar as instâncias e injetar as depedências.
return [
'Intro\WelcomeMessage\WelcomeMessageController' => App\UseCases\Intro\WelcomeMessage\WelcomeMessageFactory::class
];
De modo geral, será exemplificado a criação de um use case incluindo recursos como banco de dados.
|-- Providers
| |-- IUserPostgresProvider.php
| |-- Implementation
| | |-- UserPostgresProvider.php
|-- Repositories
| |-- UserRepository.php
|-- UseCases
| |-- User
| | |-- FetchUser
| | | |-- FetchUserController.php
| | | |-- FetchUserFactory.php
| | | |-- IFetchUserUseCase.php
| | | |-- FetchUserUseCase.php
Note
IUserPostgresProvider
será responsável por definir os contratos de consultas SQL.
<?php
namespace App\Providers;
interface IUserPostgresProvider
{
public function fetch(int $id): array;
}
Note
UserPostgresProvider
será responsável pela implementação das consultas SQL (IUserPostgresProvider
).
No construtor de UserPostgresProvider
é passado como injeção de depedência a classe PDO
.
<?php
namespace App\Providers\Implementations;
use App\Providers\IUserPostgresProvider;
use PDO;
class UserPostgresProvider implements IUserPostgresProvider
{
public function __construct(private PDO $pdo)
{
}
public function fetch(int $id): array
{
return $this->pdo->query("...");
}
}
Note
IUserRepository
será responsável por definir o contrato de persistência dos dados.
<?php
namespace App\Repositories;
interface IUserRepository
{
public function fetchUser(int $id): array;
}
Note
UserRepository
será responsável por implementar os contratos definidos por IUserRepository
.
No construtor de UserRepository
é passado como inversão de depedência a interface IUserPostgresProvider
.
<?php
namespace App\Repositories\Implementations;
use App\Providers\IUserPostgresProvider;
use App\Repositories\IUserRepository;
class UserRepository implements IUserRepository
{
public function __construct(private IUserPostgresProvider $database)
{
}
public function fetchUser(int $id): array
{
return $this->database->fetch($id);
}
}
Note
IFetchUserUseCase
será responsável por definir o contrato do use case.
<?php
namespace App\UseCases\User\FetchUser;
interface IFetchUserUseCase
{
public function execute(int $userId): array;
}
Note
FetchUserUseCase
será responsável por implementar IFetchUserUseCase
, bem como as regras de negócio e nesse caso realizar operações através do repository.
No construtor de FetchUserUseCase
é passado como inversão de depedência o IUserRepository
.
<?php
namespace App\UseCases\User\FetchUser;
use App\Repositories\IUserRepository;
use App\UseCases\User\FetchUser\IFetchUserUseCase;
use Exception;
class FetchUserUseCase implements IFetchUserUseCase
{
public function __construct(private IUserRepository $userRepository)
{
}
public function execute(int $userId): array
{
$user = $this->userRepository->fetchUser($userId);
if (!$user) {
throw new Exception('Sorry, user not found.');
}
return $user;
}
}
Note
No construtor do controller é passado como inversão de depedência a interface IFetchUserUseCase
.
<?php
namespace App\UseCases\User\FetchUser;
use App\Http\Request;
use App\Http\Response;
use App\UseCases\User\FetchUser\IFetchUserUseCase;
class FetchUserController
{
public function __construct(private IFetchUserUseCase $fetchUserUseCase)
{
}
public function handle(Request $request, Response $response): Response
{
return $response->json([
"data" => $this->fetchUserUseCase->execute($request->user()->id),
]);
}
}
Note
Por último deve-se passar as implementações dos contratos na factory do use case.
<?php
namespace App\UseCases\User\FetchUser;
use App\Infrastructure\Postgres;
use App\Providers\Implementations\UserPostgresProvider;
use App\Repositories\Implementations\UserRepository;
use App\UseCases\User\FetchUser\FetchUserController;
use App\UseCases\User\FetchUser\FetchUserUseCase;
class FetchUserFactory
{
public function generateInstance(array $databaseConfig): FetchUserController
{
$postgres = new Postgres();
$postgresProvider = new UserPostgresProvider($postgres::connect($databaseConfig));
$userRepository = new UserRepository($postgresProvider);
$fetchUserUseCase = new FetchUserUseCase($userRepository);
$fetchUserController = new FetchUserController($fetchUserUseCase);
return $fetchUserController;
}
}
Note
$databaseConfig
que é passado como paramêtro em generateInstance
, é o array definido em config/database.php
.
Se quiser mudar o provider de postgresql para mysql por exemplo, será necessário definir as configurações do novo banco.
No exemplo será mostrado a configuração do mysql com PDO, mas, caso deseje usar mysqlli ou algum ORM, basta criar a conexão como no exemplo abaixo e implementar o provider.
config/database.php
<?php
return [
'pgsql' => [
'host' => $_ENV['PG_HOST'],
'port' => $_ENV['PG_PORT'],
'database' => $_ENV['PG_DATABASE'],
'username' => $_ENV['PG_USERNAME'],
'password' => $_ENV['PG_PASSWORD'],
],
'mysql' => [
'host' => '...',
'database' => '...',
'username' => '...',
'password' => '...'
]
];
Infrastructure/Mysql.php
<?php
namespace App\Infrastructure;
use PDO;
class Mysql
{
public static function connect(array $config): PDO
{
$host = $config['mysql']['host'];
$dbname = $config['mysql']['database'];
$username = $config['mysql']['username'];
$password = $config['mysql']['password'];
$dns = "mysql:host=$host;dbname=$dbname;";
$pdo = new PDO($dns, $username, $password);
return $pdo;
}
}
Providers/IUserMysqlProvider.php
<?php
namespace App\Providers;
interface IUserMysqlProvider
{
public function save(array $fields): bool;
}
Providers/Implementations/UserMysqlProvider.php
<?php
namespace App\Providers\Implementations;
use App\Providers\IUserMysqlProvider;
use PDO;
class UserMysqlProvider implements IUserMysqlProvider
{
public function __construct(private PDO $pdo)
{
}
public function save(array $fields): bool
{
// ...
}
}
Pronto, após isso é só passar as depedências na factory do use case como nos exemplos já mostrados.
Para adicionar um novo middleware segue-se o exemplo abaixo:
|-- Middlewares
| |-- NewMiddleware.php
<?php
namespace App\Middlewares;
use App\Http\JWT;
use App\Http\Request;
use App\Http\Response;
class NewMiddleware
{
public function handle(Request $request, Response $respose)
{
$a = 1;
$b = 2;
if ($a !== $b) {
return $respose->json(['message' => 'Unauthorized'], 401);
}
}
}
O próximo passado será associar o middleware com uma chave única em config/middlewares.php
.
<?php
return [
'auth' => App\Middlewares\EnsureAuthenticatedMiddleware::class,
'equal' => App\Middlewares\NewMiddleware::class,
]
Por último é só usar o middleware na rota.
Router::get('/users/fetch', 'User\FetchUser\FetchUserController')->middlewares('auth', 'equal');
Note
Siga os passos abaixo para a execução do projeto em ambiente de desenvolvimento.
O primeiro passo, é renomear o arquivo .env.example
para .env
, o mesmo se encontra em /www
.
# Install deps www/
$ cd www && composer install
# Install deps web/
$ cd web && pnpm install
# Docker
$ docker compose -f "docker-compose-dev.yml" up -d --build
# Tests
$ cd www && composer test
Eric Neves |
|