Skip to content

Commit

Permalink
feat(wip): make routing configurable
Browse files Browse the repository at this point in the history
  • Loading branch information
Mohammad-Alavi committed Jan 1, 2025
1 parent e30f028 commit d5daef0
Show file tree
Hide file tree
Showing 16 changed files with 231 additions and 172 deletions.
37 changes: 20 additions & 17 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true">
<testsuites>
<testsuite name="Functional">
<directory>tests/Functional</directory>
</testsuite>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
</testsuites>
<php>
<env name="DB_CONNECTION" value="testing"/>
</php>
<source>
<include>
<directory>src</directory>
</include>
</source>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" bootstrap="vendor/autoload.php"
colors="true">
<testsuites>
<testsuite name="Functional">
<directory>tests/Functional</directory>
</testsuite>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
</testsuites>
<php>
<env name="DB_CONNECTION" value="testing"/>
<env name="APP_KEY" value="AckfSECXIvnK5r28GVIWUAxmbBSjTsmF"/>
</php>
<source>
<include>
<directory>src</directory>
</include>
</source>
</phpunit>
155 changes: 38 additions & 117 deletions src/Foundation/Apiato.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,11 @@

use Apiato\Foundation\Configuration\ApplicationBuilder;
use Apiato\Foundation\Configuration\Localization;
use Apiato\Foundation\Configuration\Routing;
use Apiato\Foundation\Middlewares\ProcessETag;
use Apiato\Foundation\Middlewares\Profiler;
use Apiato\Foundation\Middlewares\ValidateJsonContent;
use Apiato\Foundation\Support\PathHelper;
use Composer\Autoload\ClassLoader;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Route;
use Symfony\Component\Finder\SplFileInfo;

final class Apiato
{
Expand All @@ -23,9 +19,10 @@ final class Apiato
private array $commandPaths = [];
private array $helperPaths = [];
private Localization $localization;
private Routing $routing;

private function __construct(
private string $basePath,
private readonly string $basePath,
) {
}

Expand Down Expand Up @@ -57,7 +54,8 @@ public static function configure(string|null $basePath = null): ApplicationBuild
...glob($basePath . '/app/Containers/*/*/UI/Console', GLOB_ONLYDIR | GLOB_NOSORT),
)->withHelpers(
$basePath . '/app/Ship/Helpers',
)->withTranslations();
)->withTranslations()
->withRouting();
}

/**
Expand All @@ -71,22 +69,27 @@ public static function inferBasePath(): string
};
}

public static function instance(): self
public function withRouting(callable|null $callback = null): self
{
return self::$instance;
}
// TODO: maybe make the configuration parametrized like web: api:, like the way Laravel does it?
$this->routing = (new Routing())
->loadApiRoutesFrom(
...glob($this->basePath . '/app/Containers/*/*/UI/API/Routes', GLOB_ONLYDIR | GLOB_NOSORT),
)->loadWebRoutesFrom(
...glob($this->basePath . '/app/Containers/*/*/UI/WEB/Routes', GLOB_ONLYDIR | GLOB_NOSORT),
);

public function withProviders(string ...$path): self
{
$this->providerPaths = $path;
if (!is_null($callback)) {
$callback($this->routing);
}

return $this;
}

public function withTranslations(callable|null $callback = null): self
{
$this->localization = (new Localization())
->loadTranslationsFrom(
->loadFrom(
$this->basePath . '/app/Ship/Languages',
...glob($this->basePath . '/app/Containers/*/*/Languages', GLOB_ONLYDIR | GLOB_NOSORT),
);
Expand All @@ -98,24 +101,36 @@ public function withTranslations(callable|null $callback = null): self
return $this;
}

public function withConfigs(string ...$path): void
public function withHelpers(string ...$path): void
{
$this->configPaths = $path;
$this->helperPaths = $path;
}

public function withCommands(string ...$path): void
{
$this->commandPaths = $path;
}

public function withEvents(string ...$path): void
{
$this->listenerPaths = $path;
}

public function withCommands(string ...$path): void
public function withConfigs(string ...$path): void
{
$this->commandPaths = $path;
$this->configPaths = $path;
}

public function withHelpers(string ...$path): void
public function withProviders(string ...$path): self
{
$this->helperPaths = $path;
$this->providerPaths = $path;

return $this;
}

public static function instance(): self
{
return self::$instance;
}

public function providerPaths(): array
Expand Down Expand Up @@ -157,107 +172,13 @@ public function commands(): array
return $this->commandPaths;
}

// TODO: separate Api and Web route registration
public function registerRoutes(): void
{
$allContainerPaths = PathHelper::getContainerPaths();

foreach ($allContainerPaths as $containerPath) {
$this->loadContainerApiRoutes($containerPath);
$this->loadContainerWebRoutes($containerPath);
}
}

private function loadContainerApiRoutes(string $containerPath): void
{
$apiRoutesPath = $this->getRoutePathsForUI($containerPath, 'API');

if (File::isDirectory($apiRoutesPath)) {
$files = $this->getFilesSortedByName($apiRoutesPath);
foreach ($files as $file) {
$this->loadApiRoute($file);
}
}
}

private function getRoutePathsForUI(string $containerPath, string $ui): string
{
return $containerPath . DIRECTORY_SEPARATOR . 'UI' . DIRECTORY_SEPARATOR . $ui . DIRECTORY_SEPARATOR . 'Routes';
}

/**
* @return array|SplFileInfo[]
*/
private function getFilesSortedByName(string $apiRoutesPath): array
{
$files = File::allFiles($apiRoutesPath);

return Arr::sort($files, static fn ($file) => $file->getFilename());
}

private function loadApiRoute(SplFileInfo $file): void
{
$routeGroupArray = $this->getApiRouteGroup($file);

Route::group($routeGroupArray, static function () use ($file): void {
require $file->getPathname();
});
}

public function getApiRouteGroup(SplFileInfo|string $endpointFileOrPrefixString): array
{
return [
'middleware' => $this->getMiddlewares(),
'domain' => config('apiato.api.url'),
// If $endpointFileOrPrefixString is a string, use that string as prefix
// else, if it is a file then get the version name from the file name, and use it as prefix
'prefix' => is_string($endpointFileOrPrefixString) ? $endpointFileOrPrefixString : $this->getApiVersionPrefix($endpointFileOrPrefixString),
];
}

private function getMiddlewares(): array
{
$middlewares = ['api'];
if (config('apiato.api.rate-limiter.enabled')) {
$middlewares[] = 'throttle:' . config('apiato.api.rate-limiter.name');
}

return $middlewares;
}

private function getApiVersionPrefix(SplFileInfo $file): string
{
return config('apiato.api.prefix') . (config('apiato.api.enable_version_prefix') ? $this->getRouteFileVersionFromFileName($file) : '');
}

private function getRouteFileVersionFromFileName(SplFileInfo $file): string|bool
{
$fileNameWithoutExtension = pathinfo($file->getFilename(), PATHINFO_FILENAME);
$fileNameWithoutExtensionExploded = explode('.', $fileNameWithoutExtension);

end($fileNameWithoutExtensionExploded);

return prev($fileNameWithoutExtensionExploded);
}

private function loadContainerWebRoutes($containerPath): void
{
$webRoutesPath = $this->getRoutePathsForUI($containerPath, 'WEB');

if (File::isDirectory($webRoutesPath)) {
$files = $this->getFilesSortedByName($webRoutesPath);
foreach ($files as $file) {
$this->loadWebRoute($file);
}
}
$this->routing->registerApiRoutes();
}

private function loadWebRoute(SplFileInfo $file): void
public function webRoutes(): array
{
Route::group([
'middleware' => ['web'],
], static function () use ($file) {
require $file->getPathname();
});
return $this->routing->webRoutes();
}
}
7 changes: 7 additions & 0 deletions src/Foundation/Configuration/ApplicationBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ public function withTranslations(callable|null $callback = null): self
return $this;
}

public function withRouting(callable|null $callback = null): self
{
$this->apiato->withRouting($callback);

return $this;
}

public function create(): Apiato
{
return $this->apiato;
Expand Down
2 changes: 1 addition & 1 deletion src/Foundation/Configuration/Localization.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public function paths(): array
return $this->paths;
}

public function loadTranslationsFrom(string ...$paths): self
public function loadFrom(string ...$paths): self
{
$this->paths = $paths;

Expand Down
104 changes: 104 additions & 0 deletions src/Foundation/Configuration/Routing.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

namespace Apiato\Foundation\Configuration;

use Illuminate\Support\Arr;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Route as LaravelRoute;
use Symfony\Component\Finder\SplFileInfo;

final class Routing
{
private array $apiRouteDirs = [];
private array $webRouteDirs = [];

public function loadApiRoutesFrom(string ...$path): self
{
$this->apiRouteDirs = $path;

return $this;
}

public function loadWebRoutesFrom(string ...$path): self
{
$this->webRouteDirs = $path;

return $this;
}

public function registerApiRoutes(): void
{
collect($this->apiRouteDirs)
->map(fn ($path) => $this->getFilesSortedByName($path))
->flatten()
->each(fn (SplFileInfo $file) => $this->loadApiRoute($file));
}

/**
* @return SplFileInfo[]
*/
private function getFilesSortedByName(string $apiRoutesPath): array
{
$files = File::allFiles($apiRoutesPath);

return Arr::sort($files, static fn ($file) => $file->getFilename());
}

private function loadApiRoute(SplFileInfo $file): void
{
$routeGroupArray = $this->getApiRouteGroup($file);

LaravelRoute::group($routeGroupArray, static function () use ($file): void {
require $file->getPathname();
});
}

public function getApiRouteGroup(SplFileInfo|string $endpointFileOrPrefixString): array
{
return [
'middleware' => $this->getMiddlewares(),
'domain' => config('apiato.api.url'),
// If $endpointFileOrPrefixString is a string, use that string as prefix
// else, if it is a file then get the version name from the file name, and use it as prefix
'prefix' => is_string($endpointFileOrPrefixString) ? $endpointFileOrPrefixString : $this->getApiVersionPrefix($endpointFileOrPrefixString),
];
}

private function getMiddlewares(): array
{
$middlewares = ['api'];
if (config('apiato.api.rate-limiter.enabled')) {
$middlewares[] = 'throttle:' . config('apiato.api.rate-limiter.name');
}

return $middlewares;
}

private function getApiVersionPrefix(SplFileInfo $file): string
{
return config('apiato.api.prefix') . (config('apiato.api.enable_version_prefix') ? $this->getRouteFileVersionFromFileName($file) : '');
}

private function getRouteFileVersionFromFileName(SplFileInfo $file): string|bool
{
$fileNameWithoutExtension = pathinfo($file->getFilename(), PATHINFO_FILENAME);
$fileNameWithoutExtensionExploded = explode('.', $fileNameWithoutExtension);

end($fileNameWithoutExtensionExploded);

return prev($fileNameWithoutExtensionExploded);
}

public function webRoutes(): array
{
$files = [];
foreach ($this->webRouteDirs as $path) {
foreach (glob($path . '/*.php') as $file) {
$files[] = $file;
}
}
usort($files, 'strcmp');

return $files;
}
}
Loading

0 comments on commit d5daef0

Please sign in to comment.