fix(routes): support composed prefixes (#18)
also refactors and centralizes prefixes cleanup
This commit is contained in:
31
src/Modules/Routes/Services/Uri/Concerns/CleansUriPrefix.php
Normal file
31
src/Modules/Routes/Services/Uri/Concerns/CleansUriPrefix.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Routes\Services\Uri\Concerns;
|
||||
|
||||
trait CleansUriPrefix
|
||||
{
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
protected function parseUriPartsAfterPrefix(string $prefix, string $uri): array
|
||||
{
|
||||
$parts = explode('/', $uri);
|
||||
|
||||
$cleanParts = array_values(array_filter($parts, fn ($part): bool => $part !== ''));
|
||||
|
||||
// Convert the prefix string (e.g. "app/api" or "api") into an array of parts: ["app", "api"] or ["api"].
|
||||
$prefixParts = explode('/', trim($prefix, '/'));
|
||||
|
||||
// Iterate through each prefix part and remove matching items from the start of $parts.
|
||||
foreach ($prefixParts as $index => $prefixPart) {
|
||||
// Stop checking once we hit a mismatch.
|
||||
if (! isset($cleanParts[$index]) || $cleanParts[$index] !== $prefixPart) {
|
||||
break;
|
||||
}
|
||||
|
||||
unset($cleanParts[$index]);
|
||||
}
|
||||
|
||||
return array_values($cleanParts);
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,16 @@
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Routes\Services\Uri;
|
||||
|
||||
use Sunchayn\Nimbus\Modules\Routes\Services\Uri\Concerns\CleansUriPrefix;
|
||||
|
||||
class NonVersionedUri implements UriContract
|
||||
{
|
||||
/** @var string[] */
|
||||
private array $parts;
|
||||
use CleansUriPrefix;
|
||||
|
||||
public function __construct(
|
||||
public string $value,
|
||||
public string $routesPrefix,
|
||||
) {
|
||||
$this->parts = explode('/', $value);
|
||||
}
|
||||
) {}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
@@ -21,29 +20,23 @@ class NonVersionedUri implements UriContract
|
||||
|
||||
public function getResource(): string
|
||||
{
|
||||
// Remove empty parts from the URI and reindex array
|
||||
$cleanParts = array_values(array_filter($this->parts, fn (string $part): bool => $part !== ''));
|
||||
|
||||
$cleanParts = $this->removePrefixIfPresent($cleanParts);
|
||||
|
||||
// Extract the resource (first remaining part)
|
||||
return array_shift($cleanParts) ?? '';
|
||||
return $this->parseUriPartsAfterPrefixIfItExists()[0] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $parts
|
||||
* @return string[]
|
||||
*/
|
||||
private function removePrefixIfPresent(array $parts): array
|
||||
private function parseUriPartsAfterPrefixIfItExists(): array
|
||||
{
|
||||
if (! filled($this->routesPrefix)) {
|
||||
return $parts;
|
||||
return array_values(
|
||||
array_filter(explode('/', $this->value), fn ($part): bool => $part !== ''),
|
||||
);
|
||||
}
|
||||
|
||||
if ($parts !== [] && $parts[0] === $this->routesPrefix) {
|
||||
array_shift($parts);
|
||||
}
|
||||
|
||||
return $parts;
|
||||
return $this->parseUriPartsAfterPrefix(
|
||||
prefix: $this->routesPrefix,
|
||||
uri: $this->value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,24 @@
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Routes\Services\Uri;
|
||||
|
||||
use Sunchayn\Nimbus\Modules\Routes\Services\Uri\Concerns\CleansUriPrefix;
|
||||
|
||||
class VersionedUri implements UriContract
|
||||
{
|
||||
/** @var string[] */
|
||||
use CleansUriPrefix;
|
||||
|
||||
/**
|
||||
* Clean parts are everything after the route prefix without empty strings.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private array $cleanParts;
|
||||
|
||||
public function __construct(
|
||||
public string $value,
|
||||
public string $routesPrefix,
|
||||
) {
|
||||
$this->cleanParts = $this->parseAndCleanUri();
|
||||
$this->cleanParts = $this->parseUriPartsAfterPrefixIfItExists();
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
@@ -25,36 +33,35 @@ class VersionedUri implements UriContract
|
||||
|
||||
public function getResource(): string
|
||||
{
|
||||
// If there's a version part, skip it to get the resource
|
||||
// If there's a version part, skip it to get the resource.
|
||||
if ($this->cleanParts !== [] && $this->isVersionPart($this->cleanParts[0])) {
|
||||
return $this->cleanParts[1] ?? '';
|
||||
}
|
||||
|
||||
// If there's no version part, the first part is the resource
|
||||
// If there's no version part, the first part is the resource.
|
||||
return $this->cleanParts[0] ?? '';
|
||||
}
|
||||
|
||||
private function isVersionPart(string $part): bool
|
||||
{
|
||||
// Check if the part looks like a version (e.g., v1, v2, 1.0, etc.).
|
||||
return preg_match('/^v\d+$/', $part) || preg_match('/^\d+\.\d+$/', $part);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function parseAndCleanUri(): array
|
||||
private function parseUriPartsAfterPrefixIfItExists(): array
|
||||
{
|
||||
$parts = explode('/', $this->value);
|
||||
|
||||
// Remove empty parts from the URI and reindex array
|
||||
$cleanParts = array_values(array_filter($parts, fn ($part): bool => $part !== ''));
|
||||
|
||||
// Remove the routes prefix if present
|
||||
if ($this->routesPrefix !== '' && $cleanParts !== [] && $cleanParts[0] === $this->routesPrefix) {
|
||||
array_shift($cleanParts);
|
||||
if (! filled($this->routesPrefix)) {
|
||||
return array_values(
|
||||
array_filter(explode('/', $this->value), fn ($part): bool => $part !== ''),
|
||||
);
|
||||
}
|
||||
|
||||
return $cleanParts;
|
||||
}
|
||||
|
||||
private function isVersionPart(string $part): bool
|
||||
{
|
||||
// Check if the part looks like a version (e.g., v1, v2, 1.0, etc.)
|
||||
return preg_match('/^v\d+$/', $part) || preg_match('/^\d+\.\d+$/', $part);
|
||||
return $this->parseUriPartsAfterPrefix(
|
||||
prefix: $this->routesPrefix,
|
||||
uri: $this->value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,12 @@ class NonVersionedUriUnitTest extends TestCase
|
||||
'expectedResource' => 'users',
|
||||
];
|
||||
|
||||
yield 'uri with composed routesPrefix' => [
|
||||
'value' => '/web/api/users',
|
||||
'routesPrefix' => 'web/api',
|
||||
'expectedResource' => 'users',
|
||||
];
|
||||
|
||||
yield 'uri with routesPrefix and multiple segments' => [
|
||||
'value' => '/api/users/123',
|
||||
'routesPrefix' => 'api',
|
||||
|
||||
@@ -201,6 +201,12 @@ class VersionedUriUnitTest extends TestCase
|
||||
'expectedResource' => 'users',
|
||||
];
|
||||
|
||||
yield 'versioned uri with composed prefix extracts resource' => [
|
||||
'value' => '/cms/api/v1/users',
|
||||
'routesPrefix' => 'cms/api',
|
||||
'expectedResource' => 'users',
|
||||
];
|
||||
|
||||
yield 'versioned uri with multiple segments extracts first resource' => [
|
||||
'value' => '/v1/users/123/profile',
|
||||
'routesPrefix' => '',
|
||||
|
||||
Reference in New Issue
Block a user