docs: consolidate documentation into organized structure

- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
This commit is contained in:
2025-10-05 11:05:04 +02:00
parent 887847dde6
commit 5050c7d73a
36686 changed files with 196456 additions and 12398919 deletions

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
use App\Framework\Router\WebRoutes;
use App\Framework\Router\ApiRoutes;
use App\Framework\Router\AdminRoutes;
use App\Framework\Router\HealthRoutes;
use App\Framework\Router\MediaRoutes;
use App\Framework\Router\RouteCategory;
describe('Route Enums', function () {
describe('WebRoutes', function () {
it('implements RouteNameInterface correctly', function () {
expect(WebRoutes::HOME)->toBeInstanceOf(\App\Framework\Router\RouteNameInterface::class);
});
it('returns correct values', function () {
expect(WebRoutes::HOME->value)->toBe('home');
expect(WebRoutes::CONTACT->value)->toBe('contact');
});
it('returns correct category', function () {
expect(WebRoutes::HOME->getCategory())->toBe(RouteCategory::WEB);
});
it('returns correct route type checks', function () {
expect(WebRoutes::HOME->isWebRoute())->toBeTrue();
expect(WebRoutes::HOME->isApiRoute())->toBeFalse();
expect(WebRoutes::HOME->isAdminRoute())->toBeFalse();
expect(WebRoutes::HOME->isAuthRoute())->toBeFalse();
});
});
describe('ApiRoutes', function () {
it('implements RouteNameInterface correctly', function () {
expect(ApiRoutes::USERS_LIST)->toBeInstanceOf(\App\Framework\Router\RouteNameInterface::class);
});
it('returns correct values', function () {
expect(ApiRoutes::USERS_LIST->value)->toBe('api_users_list');
expect(ApiRoutes::HEALTH->value)->toBe('api_health');
});
it('returns correct category', function () {
expect(ApiRoutes::USERS_LIST->getCategory())->toBe(RouteCategory::API);
});
it('returns correct route type checks', function () {
expect(ApiRoutes::USERS_LIST->isApiRoute())->toBeTrue();
expect(ApiRoutes::USERS_LIST->isWebRoute())->toBeFalse();
expect(ApiRoutes::USERS_LIST->isAdminRoute())->toBeFalse();
expect(ApiRoutes::USERS_LIST->isAuthRoute())->toBeFalse();
});
});
describe('AdminRoutes', function () {
it('implements RouteNameInterface correctly', function () {
expect(AdminRoutes::DASHBOARD)->toBeInstanceOf(\App\Framework\Router\RouteNameInterface::class);
});
it('returns correct values', function () {
expect(AdminRoutes::DASHBOARD->value)->toBe('admin.dashboard');
expect(AdminRoutes::MIGRATIONS->value)->toBe('admin.migrations');
});
it('returns correct category', function () {
expect(AdminRoutes::DASHBOARD->getCategory())->toBe(RouteCategory::ADMIN);
});
it('returns correct route type checks', function () {
expect(AdminRoutes::DASHBOARD->isAdminRoute())->toBeTrue();
expect(AdminRoutes::DASHBOARD->isApiRoute())->toBeFalse();
expect(AdminRoutes::DASHBOARD->isWebRoute())->toBeFalse();
expect(AdminRoutes::DASHBOARD->isAuthRoute())->toBeFalse();
});
});
describe('HealthRoutes', function () {
it('implements RouteNameInterface correctly', function () {
expect(HealthRoutes::HEALTH_CHECK)->toBeInstanceOf(\App\Framework\Router\RouteNameInterface::class);
});
it('returns correct values', function () {
expect(HealthRoutes::HEALTH_CHECK->value)->toBe('health_check');
expect(HealthRoutes::HEALTH_LIVENESS->value)->toBe('health_liveness');
});
it('returns correct category', function () {
expect(HealthRoutes::HEALTH_CHECK->getCategory())->toBe(RouteCategory::WEB);
});
it('returns correct route type checks', function () {
expect(HealthRoutes::HEALTH_CHECK->isWebRoute())->toBeTrue();
expect(HealthRoutes::HEALTH_CHECK->isApiRoute())->toBeFalse();
expect(HealthRoutes::HEALTH_CHECK->isAdminRoute())->toBeFalse();
expect(HealthRoutes::HEALTH_CHECK->isAuthRoute())->toBeFalse();
});
});
describe('MediaRoutes', function () {
it('implements RouteNameInterface correctly', function () {
expect(MediaRoutes::SHOW_IMAGE)->toBeInstanceOf(\App\Framework\Router\RouteNameInterface::class);
});
it('returns correct values', function () {
expect(MediaRoutes::SHOW_IMAGE->value)->toBe('show_image');
});
it('returns correct category', function () {
expect(MediaRoutes::SHOW_IMAGE->getCategory())->toBe(RouteCategory::MEDIA);
});
it('returns correct route type checks', function () {
expect(MediaRoutes::SHOW_IMAGE->isWebRoute())->toBeFalse();
expect(MediaRoutes::SHOW_IMAGE->isApiRoute())->toBeFalse();
expect(MediaRoutes::SHOW_IMAGE->isAdminRoute())->toBeFalse();
expect(MediaRoutes::SHOW_IMAGE->isAuthRoute())->toBeFalse();
});
});
});
describe('RouteCategory', function () {
it('has correct values', function () {
expect(RouteCategory::WEB->value)->toBe('web');
expect(RouteCategory::API->value)->toBe('api');
expect(RouteCategory::ADMIN->value)->toBe('admin');
expect(RouteCategory::AUTH->value)->toBe('auth');
expect(RouteCategory::MEDIA->value)->toBe('media');
});
});

View File

@@ -0,0 +1,190 @@
<?php
declare(strict_types=1);
use App\Framework\Router\RouteHelper;
use App\Framework\Router\UrlGenerator;
use App\Framework\Router\WebRoutes;
use App\Framework\Router\ApiRoutes;
use App\Framework\Router\AdminRoutes;
use App\Framework\Router\HealthRoutes;
use App\Framework\Router\MediaRoutes;
describe('RouteHelper', function () {
beforeEach(function () {
$this->mockUrlGenerator = mock(UrlGenerator::class);
$this->routeHelper = new RouteHelper($this->mockUrlGenerator);
});
describe('web routes', function () {
it('generates web route URLs correctly', function () {
$this->mockUrlGenerator->shouldReceive('route')
->once()
->with(WebRoutes::HOME, [], false)
->andReturn('/');
$result = $this->routeHelper->web(WebRoutes::HOME);
expect($result)->toBe('/');
});
it('generates absolute web route URLs', function () {
$this->mockUrlGenerator->shouldReceive('route')
->once()
->with(WebRoutes::HOME, [], true)
->andReturn('https://example.com/');
$result = $this->routeHelper->absoluteWeb(WebRoutes::HOME);
expect($result)->toBe('https://example.com/');
});
it('passes parameters correctly', function () {
$params = ['id' => 123];
$this->mockUrlGenerator->shouldReceive('route')
->once()
->with(WebRoutes::CONTACT, $params, false)
->andReturn('/contact?id=123');
$result = $this->routeHelper->web(WebRoutes::CONTACT, $params);
expect($result)->toBe('/contact?id=123');
});
});
describe('API routes', function () {
it('generates API route URLs correctly', function () {
$this->mockUrlGenerator->shouldReceive('route')
->once()
->with(ApiRoutes::USERS_LIST, [], false)
->andReturn('/api/users');
$result = $this->routeHelper->api(ApiRoutes::USERS_LIST);
expect($result)->toBe('/api/users');
});
it('generates absolute API route URLs', function () {
$this->mockUrlGenerator->shouldReceive('route')
->once()
->with(ApiRoutes::USERS_LIST, [], true)
->andReturn('https://api.example.com/api/users');
$result = $this->routeHelper->absoluteApi(ApiRoutes::USERS_LIST);
expect($result)->toBe('https://api.example.com/api/users');
});
});
describe('admin routes', function () {
it('generates admin route URLs correctly', function () {
$this->mockUrlGenerator->shouldReceive('route')
->once()
->with(AdminRoutes::DASHBOARD, [], false)
->andReturn('/admin');
$result = $this->routeHelper->admin(AdminRoutes::DASHBOARD);
expect($result)->toBe('/admin');
});
it('generates absolute admin route URLs', function () {
$this->mockUrlGenerator->shouldReceive('route')
->once()
->with(AdminRoutes::DASHBOARD, [], true)
->andReturn('https://admin.example.com/admin');
$result = $this->routeHelper->absoluteAdmin(AdminRoutes::DASHBOARD);
expect($result)->toBe('https://admin.example.com/admin');
});
});
describe('health routes', function () {
it('generates health route URLs correctly', function () {
$this->mockUrlGenerator->shouldReceive('route')
->once()
->with(HealthRoutes::HEALTH_CHECK, [], false)
->andReturn('/health');
$result = $this->routeHelper->health(HealthRoutes::HEALTH_CHECK);
expect($result)->toBe('/health');
});
});
describe('media routes', function () {
it('generates media route URLs correctly', function () {
$params = ['filename' => 'test.jpg'];
$this->mockUrlGenerator->shouldReceive('route')
->once()
->with(MediaRoutes::SHOW_IMAGE, $params, false)
->andReturn('/images/test.jpg');
$result = $this->routeHelper->media(MediaRoutes::SHOW_IMAGE, $params);
expect($result)->toBe('/images/test.jpg');
});
});
describe('generic route handling', function () {
it('generates URLs for any RouteNameInterface', function () {
$this->mockUrlGenerator->shouldReceive('route')
->once()
->with(WebRoutes::HOME, [], false)
->andReturn('/');
$result = $this->routeHelper->any(WebRoutes::HOME);
expect($result)->toBe('/');
});
});
describe('current route checking', function () {
it('checks if current route matches web route', function () {
$this->mockUrlGenerator->shouldReceive('isCurrentRoute')
->once()
->with(WebRoutes::HOME)
->andReturn(true);
$result = $this->routeHelper->isCurrentWeb(WebRoutes::HOME);
expect($result)->toBeTrue();
});
it('checks if current route matches API route', function () {
$this->mockUrlGenerator->shouldReceive('isCurrentRoute')
->once()
->with(ApiRoutes::USERS_LIST)
->andReturn(false);
$result = $this->routeHelper->isCurrentApi(ApiRoutes::USERS_LIST);
expect($result)->toBeFalse();
});
it('checks if current route matches admin route', function () {
$this->mockUrlGenerator->shouldReceive('isCurrentRoute')
->once()
->with(AdminRoutes::DASHBOARD)
->andReturn(true);
$result = $this->routeHelper->isCurrentAdmin(AdminRoutes::DASHBOARD);
expect($result)->toBeTrue();
});
it('checks if current route matches any route', function () {
$this->mockUrlGenerator->shouldReceive('isCurrentRoute')
->once()
->with(HealthRoutes::HEALTH_CHECK)
->andReturn(false);
$result = $this->routeHelper->isCurrent(HealthRoutes::HEALTH_CHECK);
expect($result)->toBeFalse();
});
});
});

View File

@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
use App\Framework\Router\UrlGenerator;
use App\Framework\Router\CompiledRoutes;
use App\Framework\Router\WebRoutes;
use App\Framework\Router\ApiRoutes;
use App\Framework\Router\AdminRoutes;
use App\Framework\Http\Request;
describe('UrlGenerator with Route Enums', function () {
beforeEach(function () {
$this->mockRequest = mock(Request::class);
$this->mockCompiledRoutes = mock(CompiledRoutes::class);
$this->urlGenerator = new UrlGenerator($this->mockRequest, $this->mockCompiledRoutes);
});
describe('route generation with enums', function () {
it('generates URLs using WebRoutes enum', function () {
$mockRoute = (object) ['path' => '/'];
$this->mockCompiledRoutes->shouldReceive('getNamedRoute')
->once()
->with('home')
->andReturn($mockRoute);
$result = $this->urlGenerator->route(WebRoutes::HOME);
expect($result)->toBe('/');
});
it('generates URLs using ApiRoutes enum', function () {
$mockRoute = (object) ['path' => '/api/users'];
$this->mockCompiledRoutes->shouldReceive('getNamedRoute')
->once()
->with('api_users_list')
->andReturn($mockRoute);
$result = $this->urlGenerator->route(ApiRoutes::USERS_LIST);
expect($result)->toBe('/api/users');
});
it('generates URLs using AdminRoutes enum', function () {
$mockRoute = (object) ['path' => '/admin'];
$this->mockCompiledRoutes->shouldReceive('getNamedRoute')
->once()
->with('admin.dashboard')
->andReturn($mockRoute);
$result = $this->urlGenerator->route(AdminRoutes::DASHBOARD);
expect($result)->toBe('/admin');
});
it('generates URLs with parameters using enum', function () {
$mockRoute = (object) ['path' => '/api/users/{id}'];
$this->mockCompiledRoutes->shouldReceive('getNamedRoute')
->once()
->with('api_users_show')
->andReturn($mockRoute);
$result = $this->urlGenerator->route(ApiRoutes::USERS_SHOW, ['id' => 123]);
expect($result)->toBe('/api/users/123');
});
});
describe('backward compatibility', function () {
it('still works with string route names', function () {
$mockRoute = (object) ['path' => '/legacy'];
$this->mockCompiledRoutes->shouldReceive('getNamedRoute')
->once()
->with('legacy_route')
->andReturn($mockRoute);
$result = $this->urlGenerator->route('legacy_route');
expect($result)->toBe('/legacy');
});
});
describe('absolute URL generation', function () {
it('generates absolute URLs using enums', function () {
$mockRoute = (object) ['path' => '/'];
$this->mockCompiledRoutes->shouldReceive('getNamedRoute')
->once()
->with('home')
->andReturn($mockRoute);
$this->mockRequest->server = mock();
$this->mockRequest->server->shouldReceive('isHttps')->andReturn(true);
$this->mockRequest->server->shouldReceive('getHttpHost')->andReturn('example.com');
$result = $this->urlGenerator->absoluteRoute(WebRoutes::HOME);
expect($result)->toBe('https://example.com/');
});
});
describe('current route checking', function () {
it('checks current route using enum', function () {
$mockRoute = (object) ['path' => '/'];
$this->mockCompiledRoutes->shouldReceive('getNamedRoute')
->once()
->with('home')
->andReturn($mockRoute);
$this->mockRequest->path = '/';
$result = $this->urlGenerator->isCurrentRoute(WebRoutes::HOME);
expect($result)->toBeTrue();
});
it('returns false for non-matching enum route', function () {
$mockRoute = (object) ['path' => '/admin'];
$this->mockCompiledRoutes->shouldReceive('getNamedRoute')
->once()
->with('admin.dashboard')
->andReturn($mockRoute);
$this->mockRequest->path = '/';
$result = $this->urlGenerator->isCurrentRoute(AdminRoutes::DASHBOARD);
expect($result)->toBeFalse();
});
});
});

View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
use App\Framework\Router\ValueObjects\Placeholder;
describe('Placeholder', function () {
describe('creation', function () {
it('can be created from string', function () {
$placeholder = Placeholder::fromString('id');
expect($placeholder->getName())->toBe('id');
expect($placeholder->toString())->toBe('{id}');
expect($placeholder->isWildcard())->toBeFalse();
});
it('can be created with type', function () {
$placeholder = Placeholder::typed('id', 'int');
expect($placeholder->getName())->toBe('id');
expect($placeholder->getType())->toBe('int');
expect($placeholder->getPattern())->toBe('(\d+)');
});
it('can be created as wildcard', function () {
$placeholder = Placeholder::wildcard('path');
expect($placeholder->getName())->toBe('path');
expect($placeholder->toString())->toBe('{path*}');
expect($placeholder->isWildcard())->toBeTrue();
expect($placeholder->getPattern())->toBe('(.+?)');
});
it('validates placeholder names', function () {
expect(fn() => Placeholder::fromString(''))
->toThrow(InvalidArgumentException::class);
expect(fn() => Placeholder::fromString('123invalid'))
->toThrow(InvalidArgumentException::class);
expect(fn() => Placeholder::fromString('invalid-name'))
->toThrow(InvalidArgumentException::class);
});
it('accepts valid placeholder names', function () {
$validNames = ['id', 'userId', 'user_id', '_private', 'snake_case'];
foreach ($validNames as $name) {
$placeholder = Placeholder::fromString($name);
expect($placeholder->getName())->toBe($name);
}
});
});
describe('type patterns', function () {
it('provides correct patterns for common types', function () {
$patterns = [
'int' => '(\d+)',
'uuid' => '([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})',
'slug' => '([a-z0-9\-]+)',
'alpha' => '([a-zA-Z]+)',
'alphanumeric' => '([a-zA-Z0-9]+)',
'filename' => '([a-zA-Z0-9._\-]+)',
];
foreach ($patterns as $type => $expectedPattern) {
$placeholder = Placeholder::typed('test', $type);
expect($placeholder->getPattern())->toBe($expectedPattern);
}
});
it('falls back to default pattern for unknown types', function () {
$placeholder = Placeholder::typed('test', 'unknown');
expect($placeholder->getPattern())->toBe('([^/]+)');
});
it('allows custom patterns', function () {
$placeholder = Placeholder::typed('test', 'custom', '([a-z]{3})');
expect($placeholder->getPattern())->toBe('([a-z]{3})');
});
});
describe('string representation', function () {
it('converts to proper placeholder syntax', function () {
$regular = Placeholder::fromString('id');
$wildcard = Placeholder::wildcard('path');
expect($regular->toString())->toBe('{id}');
expect($wildcard->toString())->toBe('{path*}');
});
});
describe('pattern generation', function () {
it('returns correct regex patterns', function () {
$regular = Placeholder::fromString('id');
$wildcard = Placeholder::wildcard('path');
$typed = Placeholder::typed('id', 'int');
expect($regular->getPattern())->toBe('([^/]+)');
expect($wildcard->getPattern())->toBe('(.+?)');
expect($typed->getPattern())->toBe('(\d+)');
});
});
});

View File

@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
use App\Framework\Router\ValueObjects\Placeholder;
use App\Framework\Router\ValueObjects\RoutePath;
describe('RoutePath', function () {
describe('creation', function () {
it('can be created from elements', function () {
$path = RoutePath::fromElements('api', 'users', Placeholder::fromString('id'));
expect($path->toString())->toBe('/api/users/{id}');
expect($path->isDynamic())->toBeTrue();
expect($path->getParameterNames())->toBe(['id']);
});
it('can be created from string', function () {
$path = RoutePath::fromString('/api/users/{id}');
expect($path->toString())->toBe('/api/users/{id}');
expect($path->isDynamic())->toBeTrue();
expect($path->getParameterNames())->toBe(['id']);
});
it('handles static routes', function () {
$path = RoutePath::fromElements('api', 'health');
expect($path->toString())->toBe('/api/health');
expect($path->isStatic())->toBeTrue();
expect($path->getParameterNames())->toBe([]);
});
it('handles wildcard parameters', function () {
$path = RoutePath::fromString('/files/{path*}');
expect($path->toString())->toBe('/files/{path*}');
$placeholders = $path->getPlaceholders();
expect($placeholders)->toHaveCount(1);
expect($placeholders[0]->isWildcard())->toBeTrue();
});
it('throws on empty path', function () {
expect(fn() => RoutePath::fromElements())
->toThrow(InvalidArgumentException::class);
});
});
describe('fluent builder', function () {
it('can build paths fluently', function () {
$path = RoutePath::create()
->segment('api')
->segment('users')
->parameter('id')
->build();
expect($path->toString())->toBe('/api/users/{id}');
});
it('supports typed parameters', function () {
$path = RoutePath::create()
->segment('api')
->segment('users')
->typedParameter('id', 'uuid')
->build();
$placeholders = $path->getPlaceholders();
expect($placeholders[0]->getType())->toBe('uuid');
});
it('supports quick patterns', function () {
$path = RoutePath::create()
->segment('api')
->segment('users')
->uuid();
expect($path->toString())->toBe('/api/users/{id}');
});
});
describe('regex compilation', function () {
it('compiles static routes to regex', function () {
$path = RoutePath::fromString('/api/health');
expect($path->toRegex())->toBe('~^/api/health$~');
});
it('compiles dynamic routes to regex', function () {
$path = RoutePath::fromString('/api/users/{id}');
expect($path->toRegex())->toBe('~^/api/users/([^/]+)$~');
});
it('compiles typed parameters correctly', function () {
$path = RoutePath::fromElements(
'api',
'users',
Placeholder::typed('id', 'int')
);
expect($path->toRegex())->toBe('~^/api/users/(\d+)$~');
});
it('compiles wildcard parameters', function () {
$path = RoutePath::fromElements(
'files',
Placeholder::wildcard('path')
);
expect($path->toRegex())->toBe('~^/files/(.+?)$~');
});
});
describe('manipulation', function () {
it('can append elements', function () {
$base = RoutePath::fromElements('api', 'users');
$extended = $base->append(Placeholder::fromString('id'));
expect($extended->toString())->toBe('/api/users/{id}');
});
it('can prepend elements', function () {
$base = RoutePath::fromElements('users', Placeholder::fromString('id'));
$extended = $base->prepend('api');
expect($extended->toString())->toBe('/api/users/{id}');
});
});
describe('analysis', function () {
it('counts segments correctly', function () {
$path = RoutePath::fromString('/api/users/{id}');
expect($path->getSegmentCount())->toBe(3);
});
it('identifies placeholders', function () {
$path = RoutePath::fromString('/api/users/{id}/posts/{postId}');
$placeholders = $path->getPlaceholders();
expect($placeholders)->toHaveCount(2);
expect($path->getParameterNames())->toBe(['id', 'postId']);
});
});
describe('validation', function () {
it('validates segment content', function () {
expect(fn() => RoutePath::fromElements('api', ''))
->toThrow(InvalidArgumentException::class);
});
it('rejects invalid characters in segments', function () {
expect(fn() => RoutePath::fromElements('api', 'users{id}'))
->toThrow(InvalidArgumentException::class);
});
});
});