From affedb3d46c02df18d133ff2633a856bd2d22be4 Mon Sep 17 00:00:00 2001 From: Snider Date: Mon, 9 Mar 2026 18:56:43 +0000 Subject: [PATCH] refactor: extract Service + Client to standalone packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Core\Service → core/php-service (lthn/service) Core\Website\Service → core/php-service (lthn/service) Core\Front\Client → core/php-client (lthn/client) Co-Authored-By: Claude Opus 4.6 --- .../Front/Client/Blade/dashboard.blade.php | 24 - .../Front/Client/Blade/layouts/app.blade.php | 85 -- src/Core/Front/Client/Boot.php | 69 -- src/Core/Front/Client/README.md | 52 -- src/Core/Front/Client/Routes/client.php | 30 - src/Core/Front/Client/View/Dashboard.php | 29 - .../Service/Concerns/HasServiceVersion.php | 78 -- .../Service/Contracts/HealthCheckable.php | 80 -- .../Service/Contracts/ServiceDefinition.php | 158 ---- .../Service/Contracts/ServiceDependency.php | 150 ---- src/Core/Service/Enums/ServiceStatus.php | 96 --- src/Core/Service/HealthCheckResult.php | 110 --- .../Service/ServiceDependencyException.php | 78 -- src/Core/Service/ServiceDiscovery.php | 748 ------------------ src/Core/Service/ServiceVersion.php | 184 ----- .../Tests/Unit/HealthCheckResultTest.php | 117 --- .../Service/Tests/Unit/ServiceStatusTest.php | 102 --- .../Service/Tests/Unit/ServiceVersionTest.php | 165 ---- src/Website/Service/Boot.php | 34 - src/Website/Service/Routes/web.php | 20 - .../View/Blade/components/footer.blade.php | 34 - .../View/Blade/components/header.blade.php | 32 - .../Service/View/Blade/features.blade.php | 92 --- .../Service/View/Blade/landing.blade.php | 76 -- .../View/Blade/layouts/service.blade.php | 97 --- src/Website/Service/View/Features.php | 163 ---- src/Website/Service/View/Landing.php | 100 --- 27 files changed, 3003 deletions(-) delete mode 100644 src/Core/Front/Client/Blade/dashboard.blade.php delete mode 100644 src/Core/Front/Client/Blade/layouts/app.blade.php delete mode 100644 src/Core/Front/Client/Boot.php delete mode 100644 src/Core/Front/Client/README.md delete mode 100644 src/Core/Front/Client/Routes/client.php delete mode 100644 src/Core/Front/Client/View/Dashboard.php delete mode 100644 src/Core/Service/Concerns/HasServiceVersion.php delete mode 100644 src/Core/Service/Contracts/HealthCheckable.php delete mode 100644 src/Core/Service/Contracts/ServiceDefinition.php delete mode 100644 src/Core/Service/Contracts/ServiceDependency.php delete mode 100644 src/Core/Service/Enums/ServiceStatus.php delete mode 100644 src/Core/Service/HealthCheckResult.php delete mode 100644 src/Core/Service/ServiceDependencyException.php delete mode 100644 src/Core/Service/ServiceDiscovery.php delete mode 100644 src/Core/Service/ServiceVersion.php delete mode 100644 src/Core/Service/Tests/Unit/HealthCheckResultTest.php delete mode 100644 src/Core/Service/Tests/Unit/ServiceStatusTest.php delete mode 100644 src/Core/Service/Tests/Unit/ServiceVersionTest.php delete mode 100644 src/Website/Service/Boot.php delete mode 100644 src/Website/Service/Routes/web.php delete mode 100644 src/Website/Service/View/Blade/components/footer.blade.php delete mode 100644 src/Website/Service/View/Blade/components/header.blade.php delete mode 100644 src/Website/Service/View/Blade/features.blade.php delete mode 100644 src/Website/Service/View/Blade/landing.blade.php delete mode 100644 src/Website/Service/View/Blade/layouts/service.blade.php delete mode 100644 src/Website/Service/View/Features.php delete mode 100644 src/Website/Service/View/Landing.php diff --git a/src/Core/Front/Client/Blade/dashboard.blade.php b/src/Core/Front/Client/Blade/dashboard.blade.php deleted file mode 100644 index 9832638..0000000 --- a/src/Core/Front/Client/Blade/dashboard.blade.php +++ /dev/null @@ -1,24 +0,0 @@ -
-

Your Namespace

-

Manage your space on the internet.

- -
- {{-- Namespace Overview --}} -
-

Overview

-

Your namespace details will appear here.

-
- - {{-- Quick Actions --}} -
-

Quick Actions

-

Edit your bio, view analytics, and more.

-
- - {{-- Recent Activity --}} -
-

Recent Activity

-

Your recent activity will appear here.

-
-
-
diff --git a/src/Core/Front/Client/Blade/layouts/app.blade.php b/src/Core/Front/Client/Blade/layouts/app.blade.php deleted file mode 100644 index 33e5cdf..0000000 --- a/src/Core/Front/Client/Blade/layouts/app.blade.php +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - {{ $title ?? 'Dashboard' }} - lt.hn - - {{-- Fonts --}} - - - - {{-- Font Awesome --}} - - - @vite(['resources/css/admin.css', 'resources/js/app.js']) - @fluxAppearance - - - {{-- Header --}} -
-
-
- {{-- Logo + Bio link --}} -
- lt.hn - @if(isset($bioUrl)) - / - - {{ $bioUrl }} - - @endif -
- - {{-- Nav + User menu --}} -
- @if(isset($bioUrl)) - - @endif - - @auth - - @endauth -
-
-
-
- - {{-- Main content --}} -
-
- {{ $slot }} -
-
- - @fluxScripts - - diff --git a/src/Core/Front/Client/Boot.php b/src/Core/Front/Client/Boot.php deleted file mode 100644 index b023f04..0000000 --- a/src/Core/Front/Client/Boot.php +++ /dev/null @@ -1,69 +0,0 @@ -group('client', [ - \Illuminate\Cookie\Middleware\EncryptCookies::class, - \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, - \Illuminate\Session\Middleware\StartSession::class, - \Illuminate\View\Middleware\ShareErrorsFromSession::class, - \Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, - \Illuminate\Routing\Middleware\SubstituteBindings::class, - SecurityHeaders::class, - 'auth', - ]); - } - - public function register(): void - { - // - } - - public function boot(): void - { - // Register client:: namespace for client dashboard components - $this->loadViewsFrom(__DIR__.'/Blade', 'client'); - Blade::anonymousComponentPath(__DIR__.'/Blade', 'client'); - - // Fire ClientRoutesRegistering event for lazy-loaded modules - LifecycleEventProvider::fireClientRoutes(); - } -} diff --git a/src/Core/Front/Client/README.md b/src/Core/Front/Client/README.md deleted file mode 100644 index f9cf4c4..0000000 --- a/src/Core/Front/Client/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# Core/Front/Client - -SaaS customer dashboard for namespace owners. - -## Concept - -``` -Core/Front/Web → Public (anonymous, read-only) -Core/Front/Client → SaaS customer (authenticated, namespace owner) ← THIS -Core/Front/Admin → Backend admin (privileged) -Core/Hub → SaaS operator (Host.uk.com control plane) -``` - -## Namespace vs Workspace - -- **Namespace** = your identity, tied to a URI/handle (lt.hn/you, you.lthn) -- **Workspace** = management container (org/agency that can own multiple namespaces) -- **Personal workspace** = IS your namespace (1:1 for solo users) - -A user with just a personal workspace uses **Client** to manage their namespace. -An org workspace with multiple namespaces uses **Hub** for team management. - -## Use Cases - -- Bio page editor (lt.hn/you) -- Analytics dashboard (your stats) -- Domain management (custom domains, web3) -- Settings (profile, notifications) -- Boost purchases (expand namespace entitlements) - -## Not For - -- Team/org management (use Hub) -- Multi-namespace management (use Hub) -- Backend admin tasks (use Admin) -- Public viewing (use Web) - -## Middleware - -```php -Route::middleware('client')->group(function () { - // Namespace owner routes -}); -``` - -## Views - -```blade -@extends('client::layouts.app') - - -``` diff --git a/src/Core/Front/Client/Routes/client.php b/src/Core/Front/Client/Routes/client.php deleted file mode 100644 index 9b61a4d..0000000 --- a/src/Core/Front/Client/Routes/client.php +++ /dev/null @@ -1,30 +0,0 @@ -group(function () { - // Dashboard - Route::get('/dashboard', Dashboard::class)->name('client.dashboard'); - - // Additional routes can be registered via ClientRoutesRegistering event -}); diff --git a/src/Core/Front/Client/View/Dashboard.php b/src/Core/Front/Client/View/Dashboard.php deleted file mode 100644 index 9e90cbc..0000000 --- a/src/Core/Front/Client/View/Dashboard.php +++ /dev/null @@ -1,29 +0,0 @@ -layout('client::layouts.app'); - } -} diff --git a/src/Core/Service/Concerns/HasServiceVersion.php b/src/Core/Service/Concerns/HasServiceVersion.php deleted file mode 100644 index 7f061e0..0000000 --- a/src/Core/Service/Concerns/HasServiceVersion.php +++ /dev/null @@ -1,78 +0,0 @@ -=1.0.0'), - * ]; - * } - * } - * ``` - */ -trait HasServiceVersion -{ - /** - * Get the service contract version. - * - * Override this method to specify a custom version or deprecation. - */ - public static function version(): ServiceVersion - { - return ServiceVersion::initial(); - } - - /** - * Get the service dependencies. - * - * Override this method to declare dependencies on other services. - * By default, services have no dependencies. - * - * @return array - */ - public static function dependencies(): array - { - return []; - } -} diff --git a/src/Core/Service/Contracts/HealthCheckable.php b/src/Core/Service/Contracts/HealthCheckable.php deleted file mode 100644 index fd37836..0000000 --- a/src/Core/Service/Contracts/HealthCheckable.php +++ /dev/null @@ -1,80 +0,0 @@ -database->select('SELECT 1'); - * $responseTime = (microtime(true) - $start) * 1000; - * - * if ($responseTime > 1000) { - * return HealthCheckResult::degraded( - * 'Database responding slowly', - * ['response_time_ms' => $responseTime] - * ); - * } - * - * return HealthCheckResult::healthy( - * 'All systems operational', - * responseTimeMs: $responseTime - * ); - * } catch (\Exception $e) { - * return HealthCheckResult::fromException($e); - * } - * } - * ``` - * - * - * @see HealthCheckResult For result factory methods - */ -interface HealthCheckable -{ - /** - * Perform a health check and return the result. - * - * Implementations should catch all exceptions and return - * an appropriate HealthCheckResult rather than throwing. - */ - public function healthCheck(): HealthCheckResult; -} diff --git a/src/Core/Service/Contracts/ServiceDefinition.php b/src/Core/Service/Contracts/ServiceDefinition.php deleted file mode 100644 index 715a32e..0000000 --- a/src/Core/Service/Contracts/ServiceDefinition.php +++ /dev/null @@ -1,158 +0,0 @@ -deprecate()` | - * | Sunset | Service no longer available | `version()->isPastSunset()` | - * - * ## Service Definition Array - * - * The `definition()` method returns an array with service metadata: - * - * ```php - * public static function definition(): array - * { - * return [ - * 'code' => 'bio', // Unique service code - * 'module' => 'Mod\\Bio', // Module namespace - * 'name' => 'BioHost', // Display name - * 'tagline' => 'Link in bio pages', // Short description - * 'description' => 'Create beautiful...', // Full description - * 'icon' => 'link', // FontAwesome icon - * 'color' => '#3B82F6', // Brand color - * 'entitlement_code' => 'core.srv.bio', // Access control code - * 'sort_order' => 10, // Menu ordering - * ]; - * } - * ``` - * - * ## Versioning - * - * Services should implement `version()` to declare their contract version. - * This enables tracking breaking changes and deprecation: - * - * ```php - * public static function version(): ServiceVersion - * { - * return new ServiceVersion(2, 1, 0); - * } - * - * // For deprecated services: - * public static function version(): ServiceVersion - * { - * return (new ServiceVersion(1, 0, 0)) - * ->deprecate('Use ServiceV2 instead', new \DateTimeImmutable('2025-06-01')); - * } - * ``` - * - * ## Health Monitoring - * - * For services that need health monitoring, also implement `HealthCheckable`: - * - * ```php - * class MyService implements ServiceDefinition, HealthCheckable - * { - * public function healthCheck(): HealthCheckResult - * { - * return HealthCheckResult::healthy('All systems operational'); - * } - * } - * ``` - * - * - * @see AdminMenuProvider For menu integration - * @see ServiceVersion For versioning - * @see ServiceDependency For declaring dependencies - * @see HealthCheckable For health monitoring - * @see ServiceDiscovery For the discovery and resolution process - */ -interface ServiceDefinition extends AdminMenuProvider -{ - /** - * Get the service definition for seeding platform_services. - * - * @return array{ - * code: string, - * module: string, - * name: string, - * tagline?: string, - * description?: string, - * icon?: string, - * color?: string, - * entitlement_code?: string, - * sort_order?: int, - * } - */ - public static function definition(): array; - - /** - * Get the service contract version. - * - * Implementations should return a ServiceVersion indicating the - * current version of this service's contract. This is used for: - * - Compatibility checking between service consumers and providers - * - Deprecation tracking and sunset enforcement - * - Migration planning when breaking changes are introduced - * - * Default implementation returns version 1.0.0 for backward - * compatibility with existing services. - */ - public static function version(): ServiceVersion; - - /** - * Get the service dependencies. - * - * Declare other services that this service depends on. The framework - * uses this information to: - * - Validate that required dependencies are available at boot time - * - Resolve services in correct dependency order - * - Detect circular dependencies - * - * ## Example - * - * ```php - * public static function dependencies(): array - * { - * return [ - * ServiceDependency::required('auth', '>=1.0.0'), - * ServiceDependency::optional('analytics'), - * ]; - * } - * ``` - * - * @return array - */ - public static function dependencies(): array; -} diff --git a/src/Core/Service/Contracts/ServiceDependency.php b/src/Core/Service/Contracts/ServiceDependency.php deleted file mode 100644 index 296eaba..0000000 --- a/src/Core/Service/Contracts/ServiceDependency.php +++ /dev/null @@ -1,150 +0,0 @@ -=1.0.0'), - * - * // Optional dependency on analytics - * ServiceDependency::optional('analytics'), - * - * // Required with minimum version - * ServiceDependency::required('billing', '>=2.0.0', '<3.0.0'), - * ]; - * } - * ``` - */ -final readonly class ServiceDependency -{ - /** - * @param string $serviceCode The code of the required service - * @param bool $required Whether this dependency is required - * @param string|null $minVersion Minimum version constraint (e.g., ">=1.0.0") - * @param string|null $maxVersion Maximum version constraint (e.g., "<3.0.0") - */ - public function __construct( - public string $serviceCode, - public bool $required = true, - public ?string $minVersion = null, - public ?string $maxVersion = null, - ) {} - - /** - * Create a required dependency. - */ - public static function required( - string $serviceCode, - ?string $minVersion = null, - ?string $maxVersion = null - ): self { - return new self($serviceCode, true, $minVersion, $maxVersion); - } - - /** - * Create an optional dependency. - */ - public static function optional( - string $serviceCode, - ?string $minVersion = null, - ?string $maxVersion = null - ): self { - return new self($serviceCode, false, $minVersion, $maxVersion); - } - - /** - * Check if a version satisfies this dependency's constraints. - */ - public function satisfiedBy(string $version): bool - { - if ($this->minVersion !== null && ! $this->checkConstraint($version, $this->minVersion)) { - return false; - } - - if ($this->maxVersion !== null && ! $this->checkConstraint($version, $this->maxVersion)) { - return false; - } - - return true; - } - - /** - * Check a single version constraint. - */ - protected function checkConstraint(string $version, string $constraint): bool - { - // Parse constraint (e.g., ">=1.0.0" or "<3.0.0") - if (preg_match('/^([<>=!]+)?(.+)$/', $constraint, $matches)) { - $operator = $matches[1] ?: '=='; - $constraintVersion = ltrim($matches[2], 'v'); - $version = ltrim($version, 'v'); - - return version_compare($version, $constraintVersion, $operator); - } - - return true; - } - - /** - * Get a human-readable description of the constraint. - */ - public function getConstraintDescription(): string - { - $parts = []; - - if ($this->minVersion !== null) { - $parts[] = $this->minVersion; - } - - if ($this->maxVersion !== null) { - $parts[] = $this->maxVersion; - } - - if (empty($parts)) { - return 'any version'; - } - - return implode(' ', $parts); - } - - /** - * Convert to array for serialization. - * - * @return array - */ - public function toArray(): array - { - return array_filter([ - 'service_code' => $this->serviceCode, - 'required' => $this->required, - 'min_version' => $this->minVersion, - 'max_version' => $this->maxVersion, - ], fn ($v) => $v !== null); - } -} diff --git a/src/Core/Service/Enums/ServiceStatus.php b/src/Core/Service/Enums/ServiceStatus.php deleted file mode 100644 index a5bf9ea..0000000 --- a/src/Core/Service/Enums/ServiceStatus.php +++ /dev/null @@ -1,96 +0,0 @@ - true, - self::UNHEALTHY, self::UNKNOWN => false, - }; - } - - /** - * Get a human-readable label for the status. - */ - public function label(): string - { - return match ($this) { - self::HEALTHY => 'Healthy', - self::DEGRADED => 'Degraded', - self::UNHEALTHY => 'Unhealthy', - self::UNKNOWN => 'Unknown', - }; - } - - /** - * Get the severity level for logging/alerting. - * Lower values = more severe. - */ - public function severity(): int - { - return match ($this) { - self::HEALTHY => 0, - self::DEGRADED => 1, - self::UNHEALTHY => 2, - self::UNKNOWN => 3, - }; - } - - /** - * Create status from a boolean health check result. - */ - public static function fromBoolean(bool $healthy): self - { - return $healthy ? self::HEALTHY : self::UNHEALTHY; - } - - /** - * Get the worst status from multiple statuses. - * - * @param array $statuses - */ - public static function worst(array $statuses): self - { - if (empty($statuses)) { - return self::UNKNOWN; - } - - $worstSeverity = -1; - $worstStatus = self::HEALTHY; - - foreach ($statuses as $status) { - if ($status->severity() > $worstSeverity) { - $worstSeverity = $status->severity(); - $worstStatus = $status; - } - } - - return $worstStatus; - } -} diff --git a/src/Core/Service/HealthCheckResult.php b/src/Core/Service/HealthCheckResult.php deleted file mode 100644 index 6f044f3..0000000 --- a/src/Core/Service/HealthCheckResult.php +++ /dev/null @@ -1,110 +0,0 @@ - $data Additional diagnostic data - */ - public function __construct( - public ServiceStatus $status, - public string $message = '', - public array $data = [], - public ?float $responseTimeMs = null, - ) {} - - /** - * Create a healthy result. - * - * @param array $data - */ - public static function healthy(string $message = 'Service is healthy', array $data = [], ?float $responseTimeMs = null): self - { - return new self(ServiceStatus::HEALTHY, $message, $data, $responseTimeMs); - } - - /** - * Create a degraded result. - * - * @param array $data - */ - public static function degraded(string $message, array $data = [], ?float $responseTimeMs = null): self - { - return new self(ServiceStatus::DEGRADED, $message, $data, $responseTimeMs); - } - - /** - * Create an unhealthy result. - * - * @param array $data - */ - public static function unhealthy(string $message, array $data = [], ?float $responseTimeMs = null): self - { - return new self(ServiceStatus::UNHEALTHY, $message, $data, $responseTimeMs); - } - - /** - * Create an unknown status result. - * - * @param array $data - */ - public static function unknown(string $message = 'Health check not available', array $data = []): self - { - return new self(ServiceStatus::UNKNOWN, $message, $data); - } - - /** - * Create a result from an exception. - */ - public static function fromException(\Throwable $e): self - { - return self::unhealthy( - message: $e->getMessage(), - data: [ - 'exception' => get_class($e), - 'code' => $e->getCode(), - ] - ); - } - - /** - * Check if the result indicates operational status. - */ - public function isOperational(): bool - { - return $this->status->isOperational(); - } - - /** - * Convert to array for JSON serialization. - * - * @return array - */ - public function toArray(): array - { - return array_filter([ - 'status' => $this->status->value, - 'message' => $this->message, - 'data' => $this->data ?: null, - 'response_time_ms' => $this->responseTimeMs, - ], fn ($v) => $v !== null); - } -} diff --git a/src/Core/Service/ServiceDependencyException.php b/src/Core/Service/ServiceDependencyException.php deleted file mode 100644 index 19d21c7..0000000 --- a/src/Core/Service/ServiceDependencyException.php +++ /dev/null @@ -1,78 +0,0 @@ - - */ - protected array $dependencyChain = []; - - /** - * Create exception for circular dependency. - * - * @param array $chain The chain of services that form the cycle - */ - public static function circular(array $chain): self - { - $exception = new self( - 'Circular dependency detected: '.implode(' -> ', $chain) - ); - $exception->dependencyChain = $chain; - - return $exception; - } - - /** - * Create exception for missing required dependency. - */ - public static function missing(string $service, string $dependency): self - { - return new self( - "Service '{$service}' requires '{$dependency}' which is not available" - ); - } - - /** - * Create exception for version constraint not satisfied. - */ - public static function versionMismatch( - string $service, - string $dependency, - string $required, - string $available - ): self { - return new self( - "Service '{$service}' requires '{$dependency}' {$required}, but {$available} is installed" - ); - } - - /** - * Get the dependency chain that caused the error. - * - * @return array - */ - public function getDependencyChain(): array - { - return $this->dependencyChain; - } -} diff --git a/src/Core/Service/ServiceDiscovery.php b/src/Core/Service/ServiceDiscovery.php deleted file mode 100644 index e2e03f8..0000000 --- a/src/Core/Service/ServiceDiscovery.php +++ /dev/null @@ -1,748 +0,0 @@ - 'billing', - * 'module' => 'Mod\\Billing', - * 'name' => 'Billing Service', - * 'tagline' => 'Handle payments and subscriptions', - * 'icon' => 'credit-card', - * 'color' => '#10B981', - * ]; - * } - * - * public static function dependencies(): array - * { - * return [ - * ServiceDependency::required('auth', '>=1.0.0'), - * ServiceDependency::optional('analytics'), - * ]; - * } - * - * public function healthCheck(): HealthCheckResult - * { - * // Check payment provider connectivity - * return HealthCheckResult::healthy(); - * } - * } - * ``` - * - * ## Discovery Process - * - * 1. Scans module paths for classes implementing ServiceDefinition - * 2. Validates each service's dependencies are available - * 3. Detects circular dependencies - * 4. Returns services in dependency-resolved order - * - * ## Caching - * - * Discovery results are cached for 1 hour (configurable). Clear the cache when - * adding new services or modifying dependencies: - * - * ```php - * $discovery->clearCache(); - * ``` - * - * Caching can be disabled via config: `core.services.cache_discovery => false` - * - * ## Example Usage - * - * ```php - * $discovery = app(ServiceDiscovery::class); - * - * // Get all registered services - * $services = $discovery->discover(); - * - * // Get services in dependency order - * $ordered = $discovery->getResolutionOrder(); - * - * // Check if a service is available - * $available = $discovery->has('billing'); - * - * // Get a specific service definition class - * $billingClass = $discovery->get('billing'); - * - * // Get instantiated service - * $billing = $discovery->getInstance('billing'); - * - * // Validate all dependencies are satisfied - * $missing = $discovery->validateDependencies(); - * if (!empty($missing)) { - * foreach ($missing as $service => $deps) { - * logger()->error("Service {$service} missing: " . implode(', ', $deps)); - * } - * } - * ``` - * - * - * @see ServiceDefinition For the service contract interface - * @see ServiceVersion For versioning and deprecation - * @see ServiceDependency For declaring dependencies - * @see HealthCheckable For service health monitoring - */ -class ServiceDiscovery -{ - /** - * Cache key for discovered services. - */ - protected const CACHE_KEY = 'core.services.discovered'; - - /** - * Cache TTL in seconds (1 hour by default). - */ - protected const CACHE_TTL = 3600; - - /** - * Discovered service definitions indexed by service code. - * - * @var array> - */ - protected array $services = []; - - /** - * Whether discovery has been performed. - */ - protected bool $discovered = false; - - /** - * Additional paths to scan for services. - * - * @var array - */ - protected array $additionalPaths = []; - - /** - * Manually registered service classes. - * - * @var array> - */ - protected array $registered = []; - - /** - * Add a path to scan for service definitions. - */ - public function addPath(string $path): self - { - $this->additionalPaths[] = $path; - $this->discovered = false; - - return $this; - } - - /** - * Manually register a service definition class. - * - * Validates the service definition before registering. The validation checks: - * - Class exists and implements ServiceDefinition - * - Definition array contains required 'code' field - * - Definition array contains required 'module' field - * - Definition array contains required 'name' field - * - Code is a non-empty string with valid format - * - * @param class-string $serviceClass - * @param bool $validate Whether to validate the service definition (default: true) - * - * @throws \InvalidArgumentException If validation fails - */ - public function register(string $serviceClass, bool $validate = true): self - { - if ($validate) { - $this->validateServiceClass($serviceClass); - } - - $this->registered[] = $serviceClass; - $this->discovered = false; - - return $this; - } - - /** - * Validate a service definition class. - * - * @param class-string $serviceClass - * - * @throws \InvalidArgumentException If validation fails - */ - public function validateServiceClass(string $serviceClass): void - { - // Check class exists - if (! class_exists($serviceClass)) { - throw new \InvalidArgumentException( - "Service class '{$serviceClass}' does not exist" - ); - } - - // Check implements interface - if (! is_subclass_of($serviceClass, ServiceDefinition::class)) { - throw new \InvalidArgumentException( - "Service class '{$serviceClass}' must implement ".ServiceDefinition::class - ); - } - - // Validate definition array - try { - $definition = $serviceClass::definition(); - } catch (\Throwable $e) { - throw new \InvalidArgumentException( - "Service class '{$serviceClass}' definition() method threw an exception: ".$e->getMessage() - ); - } - - $this->validateDefinitionArray($serviceClass, $definition); - } - - /** - * Validate a service definition array. - * - * - * @throws \InvalidArgumentException If validation fails - */ - protected function validateDefinitionArray(string $serviceClass, array $definition): void - { - $requiredFields = ['code', 'module', 'name']; - $missingFields = []; - - foreach ($requiredFields as $field) { - if (! isset($definition[$field]) || $definition[$field] === '') { - $missingFields[] = $field; - } - } - - if (! empty($missingFields)) { - throw new \InvalidArgumentException( - "Service '{$serviceClass}' definition() is missing required fields: ".implode(', ', $missingFields) - ); - } - - // Validate code format (should be a simple identifier) - if (! is_string($definition['code'])) { - throw new \InvalidArgumentException( - "Service '{$serviceClass}' code must be a string" - ); - } - - if (! preg_match('/^[a-z][a-z0-9_-]*$/i', $definition['code'])) { - throw new \InvalidArgumentException( - "Service '{$serviceClass}' code '{$definition['code']}' is invalid. ". - 'Code must start with a letter and contain only letters, numbers, underscores, and hyphens.' - ); - } - - // Validate module namespace format - if (! is_string($definition['module'])) { - throw new \InvalidArgumentException( - "Service '{$serviceClass}' module must be a string" - ); - } - - // Validate optional fields if present - if (isset($definition['sort_order']) && ! is_int($definition['sort_order'])) { - throw new \InvalidArgumentException( - "Service '{$serviceClass}' sort_order must be an integer" - ); - } - - if (isset($definition['color']) && ! $this->isValidColor($definition['color'])) { - throw new \InvalidArgumentException( - "Service '{$serviceClass}' color '{$definition['color']}' is invalid. ". - 'Color must be a valid hex color (e.g., #3B82F6)' - ); - } - } - - /** - * Check if a color string is a valid hex color. - */ - protected function isValidColor(mixed $color): bool - { - if (! is_string($color)) { - return false; - } - - return (bool) preg_match('/^#[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/', $color); - } - - /** - * Validate all registered services and return validation errors. - * - * @return array> Array of service code => validation errors - */ - public function validateAll(): array - { - $this->discover(); - $errors = []; - - foreach ($this->services as $code => $class) { - try { - $this->validateServiceClass($class); - } catch (\InvalidArgumentException $e) { - $errors[$code][] = $e->getMessage(); - } - - // Also validate dependencies - $depErrors = $this->validateServiceDependenciesForService($code, $class); - if (! empty($depErrors)) { - $errors[$code] = array_merge($errors[$code] ?? [], $depErrors); - } - } - - return $errors; - } - - /** - * Validate dependencies for a specific service. - * - * @param class-string $class - * @return array - */ - protected function validateServiceDependenciesForService(string $code, string $class): array - { - $errors = []; - $dependencies = $this->getServiceDependencies($class); - - foreach ($dependencies as $dependency) { - if ($dependency->required && ! $this->has($dependency->serviceCode)) { - $errors[] = "Required dependency '{$dependency->serviceCode}' is not available"; - } - - // Check version constraint - if ($this->has($dependency->serviceCode)) { - $depClass = $this->get($dependency->serviceCode); - if ($depClass !== null) { - $version = $depClass::version()->toString(); - if (! $dependency->satisfiedBy($version)) { - $errors[] = sprintf( - "Dependency '%s' version mismatch: requires %s, found %s", - $dependency->serviceCode, - $dependency->getConstraintDescription(), - $version - ); - } - } - } - } - - return $errors; - } - - /** - * Discover all service definitions. - * - * @return Collection> - */ - public function discover(): Collection - { - if ($this->discovered) { - return collect($this->services); - } - - // Try to load from cache - $cached = $this->loadFromCache(); - if ($cached !== null) { - $this->services = $cached; - $this->discovered = true; - - return collect($this->services); - } - - // Perform discovery - $this->services = $this->performDiscovery(); - $this->discovered = true; - - // Cache results - $this->saveToCache($this->services); - - return collect($this->services); - } - - /** - * Check if a service is available. - */ - public function has(string $serviceCode): bool - { - $this->discover(); - - return isset($this->services[$serviceCode]); - } - - /** - * Get a service definition class by code. - * - * @return class-string|null - */ - public function get(string $serviceCode): ?string - { - $this->discover(); - - return $this->services[$serviceCode] ?? null; - } - - /** - * Get service instance by code. - */ - public function getInstance(string $serviceCode): ?ServiceDefinition - { - $class = $this->get($serviceCode); - if ($class === null) { - return null; - } - - return app($class); - } - - /** - * Get services in dependency resolution order. - * - * Services are ordered so that dependencies come before dependents. - * - * @return Collection> - * - * @throws ServiceDependencyException If circular dependency detected - */ - public function getResolutionOrder(): Collection - { - $this->discover(); - - $resolved = []; - $resolving = []; - - foreach ($this->services as $code => $class) { - $this->resolveService($code, $resolved, $resolving); - } - - return collect($resolved); - } - - /** - * Validate all service dependencies. - * - * @return array> Array of service code => missing dependencies - */ - public function validateDependencies(): array - { - $this->discover(); - $missing = []; - - foreach ($this->services as $code => $class) { - $dependencies = $this->getServiceDependencies($class); - - foreach ($dependencies as $dependency) { - if (! $dependency->required) { - continue; - } - - if (! $this->has($dependency->serviceCode)) { - $missing[$code][] = $dependency->serviceCode; - - continue; - } - - // Check version constraint - $depClass = $this->get($dependency->serviceCode); - if ($depClass !== null) { - $version = $depClass::version()->toString(); - if (! $dependency->satisfiedBy($version)) { - $missing[$code][] = sprintf( - '%s (%s required, %s available)', - $dependency->serviceCode, - $dependency->getConstraintDescription(), - $version - ); - } - } - } - } - - return $missing; - } - - /** - * Clear the discovery cache. - */ - public function clearCache(): void - { - Cache::forget(self::CACHE_KEY); - $this->discovered = false; - $this->services = []; - } - - /** - * Perform the actual discovery process. - * - * @return array> - */ - protected function performDiscovery(): array - { - $services = []; - - // Scan configured module paths - $paths = array_merge( - config('core.module_paths', []), - $this->additionalPaths - ); - - foreach ($paths as $path) { - if (! is_dir($path)) { - continue; - } - - $this->scanPath($path, $services); - } - - // Add manually registered services - foreach ($this->registered as $class) { - if (! class_exists($class)) { - continue; - } - - if (! is_subclass_of($class, ServiceDefinition::class)) { - continue; - } - - $definition = $class::definition(); - $code = $definition['code'] ?? null; - - if ($code !== null) { - $services[$code] = $class; - } - } - - return $services; - } - - /** - * Scan a path for service definitions. - * - * @param array> $services - */ - protected function scanPath(string $path, array &$services): void - { - $files = File::allFiles($path); - - foreach ($files as $file) { - if ($file->getExtension() !== 'php') { - continue; - } - - // Skip test files - if (str_contains($file->getPathname(), '/Tests/')) { - continue; - } - - // Skip vendor directories - if (str_contains($file->getPathname(), '/vendor/')) { - continue; - } - - $class = $this->getClassFromFile($file->getPathname()); - if ($class === null) { - continue; - } - - if (! class_exists($class)) { - continue; - } - - if (! is_subclass_of($class, ServiceDefinition::class)) { - continue; - } - - try { - $definition = $class::definition(); - $code = $definition['code'] ?? null; - - if ($code !== null) { - $services[$code] = $class; - } - } catch (\Throwable) { - // Skip services that throw during definition() - } - } - } - - /** - * Extract class name from a PHP file. - */ - protected function getClassFromFile(string $filePath): ?string - { - $contents = file_get_contents($filePath); - if ($contents === false) { - return null; - } - - $namespace = null; - $class = null; - - // Extract namespace - if (preg_match('/namespace\s+([^;]+);/', $contents, $matches)) { - $namespace = $matches[1]; - } - - // Extract class name - if (preg_match('/class\s+(\w+)/', $contents, $matches)) { - $class = $matches[1]; - } - - if ($namespace !== null && $class !== null) { - return $namespace.'\\'.$class; - } - - return null; - } - - /** - * Get dependencies for a service class. - * - * @param class-string $class - * @return array - */ - protected function getServiceDependencies(string $class): array - { - if (! method_exists($class, 'dependencies')) { - return []; - } - - return $class::dependencies(); - } - - /** - * Resolve a service and its dependencies. - * - * @param array> $resolved - * @param array $resolving - * - * @throws ServiceDependencyException If circular dependency detected - */ - protected function resolveService(string $code, array &$resolved, array &$resolving): void - { - if (in_array($code, $resolving)) { - throw new ServiceDependencyException( - 'Circular dependency detected: '.implode(' -> ', [...$resolving, $code]) - ); - } - - $class = $this->services[$code] ?? null; - if ($class === null || in_array($class, $resolved)) { - return; - } - - $resolving[] = $code; - - foreach ($this->getServiceDependencies($class) as $dependency) { - if ($this->has($dependency->serviceCode)) { - $this->resolveService($dependency->serviceCode, $resolved, $resolving); - } - } - - array_pop($resolving); - $resolved[] = $class; - } - - /** - * Load discovered services from cache. - * - * @return array>|null - */ - protected function loadFromCache(): ?array - { - if (! config('core.services.cache_discovery', true)) { - return null; - } - - return Cache::get(self::CACHE_KEY); - } - - /** - * Save discovered services to cache. - * - * @param array> $services - */ - protected function saveToCache(array $services): void - { - if (! config('core.services.cache_discovery', true)) { - return; - } - - Cache::put(self::CACHE_KEY, $services, self::CACHE_TTL); - } -} diff --git a/src/Core/Service/ServiceVersion.php b/src/Core/Service/ServiceVersion.php deleted file mode 100644 index 59a2b4d..0000000 --- a/src/Core/Service/ServiceVersion.php +++ /dev/null @@ -1,184 +0,0 @@ - [Deprecated] ──isPastSunset()──> [Sunset] - * ``` - * - * - **Active**: Service is fully supported - * - **Deprecated**: Service works but consumers should migrate - * - **Sunset**: Service should no longer be used (past sunset date) - * - * ## Usage Examples - * - * ```php - * // Create a version - * $version = new ServiceVersion(2, 1, 0); - * echo $version; // "2.1.0" - * - * // Parse from string - * $version = ServiceVersion::fromString('v2.1.0'); - * - * // Mark as deprecated with sunset date - * $version = (new ServiceVersion(1, 0, 0)) - * ->deprecate( - * 'Migrate to v2.x - see docs/migration.md', - * new \DateTimeImmutable('2025-06-01') - * ); - * - * // Check compatibility - * $minimum = new ServiceVersion(1, 5, 0); - * $current = new ServiceVersion(1, 8, 2); - * $current->isCompatibleWith($minimum); // true (same major, >= minor) - * - * // Check if past sunset - * if ($version->isPastSunset()) { - * throw new ServiceSunsetException('Service no longer available'); - * } - * ``` - */ -final readonly class ServiceVersion -{ - public function __construct( - public int $major, - public int $minor = 0, - public int $patch = 0, - public bool $deprecated = false, - public ?string $deprecationMessage = null, - public ?\DateTimeInterface $sunsetDate = null, - ) {} - - /** - * Create a version from a string (e.g., "1.2.3"). - */ - public static function fromString(string $version): self - { - $parts = explode('.', ltrim($version, 'v')); - - return new self( - major: (int) ($parts[0] ?? 1), - minor: (int) ($parts[1] ?? 0), - patch: (int) ($parts[2] ?? 0), - ); - } - - /** - * Create a version 1.0.0 instance. - */ - public static function initial(): self - { - return new self(1, 0, 0); - } - - /** - * Mark this version as deprecated. - */ - public function deprecate(string $message, ?\DateTimeInterface $sunsetDate = null): self - { - return new self( - major: $this->major, - minor: $this->minor, - patch: $this->patch, - deprecated: true, - deprecationMessage: $message, - sunsetDate: $sunsetDate, - ); - } - - /** - * Check if the version is past its sunset date. - */ - public function isPastSunset(): bool - { - if ($this->sunsetDate === null) { - return false; - } - - return $this->sunsetDate < new \DateTimeImmutable; - } - - /** - * Compare with another version. - * - * @return int -1 if less, 0 if equal, 1 if greater - */ - public function compare(self $other): int - { - if ($this->major !== $other->major) { - return $this->major <=> $other->major; - } - - if ($this->minor !== $other->minor) { - return $this->minor <=> $other->minor; - } - - return $this->patch <=> $other->patch; - } - - /** - * Check if this version is compatible with a minimum version. - * Compatible if same major version and >= minor.patch. - */ - public function isCompatibleWith(self $minimum): bool - { - if ($this->major !== $minimum->major) { - return false; - } - - return $this->compare($minimum) >= 0; - } - - /** - * Get version as string. - */ - public function toString(): string - { - return "{$this->major}.{$this->minor}.{$this->patch}"; - } - - public function __toString(): string - { - return $this->toString(); - } - - /** - * Convert to array for serialization. - * - * @return array - */ - public function toArray(): array - { - return array_filter([ - 'version' => $this->toString(), - 'deprecated' => $this->deprecated ?: null, - 'deprecation_message' => $this->deprecationMessage, - 'sunset_date' => $this->sunsetDate?->format('Y-m-d'), - ], fn ($v) => $v !== null); - } -} diff --git a/src/Core/Service/Tests/Unit/HealthCheckResultTest.php b/src/Core/Service/Tests/Unit/HealthCheckResultTest.php deleted file mode 100644 index 3e005a2..0000000 --- a/src/Core/Service/Tests/Unit/HealthCheckResultTest.php +++ /dev/null @@ -1,117 +0,0 @@ - 'value'], 15.5); - - $this->assertSame(ServiceStatus::HEALTHY, $result->status); - $this->assertSame('All good', $result->message); - $this->assertSame(['key' => 'value'], $result->data); - $this->assertSame(15.5, $result->responseTimeMs); - } - - #[Test] - public function it_creates_degraded_result(): void - { - $result = HealthCheckResult::degraded('Slow response'); - - $this->assertSame(ServiceStatus::DEGRADED, $result->status); - $this->assertSame('Slow response', $result->message); - } - - #[Test] - public function it_creates_unhealthy_result(): void - { - $result = HealthCheckResult::unhealthy('Connection failed'); - - $this->assertSame(ServiceStatus::UNHEALTHY, $result->status); - $this->assertSame('Connection failed', $result->message); - } - - #[Test] - public function it_creates_unknown_result(): void - { - $result = HealthCheckResult::unknown(); - - $this->assertSame(ServiceStatus::UNKNOWN, $result->status); - $this->assertSame('Health check not available', $result->message); - } - - #[Test] - public function it_creates_from_exception(): void - { - $exception = new \RuntimeException('Database error', 500); - $result = HealthCheckResult::fromException($exception); - - $this->assertSame(ServiceStatus::UNHEALTHY, $result->status); - $this->assertSame('Database error', $result->message); - $this->assertSame('RuntimeException', $result->data['exception']); - $this->assertSame(500, $result->data['code']); - } - - #[Test] - public function it_checks_operational_status(): void - { - $this->assertTrue(HealthCheckResult::healthy()->isOperational()); - $this->assertTrue(HealthCheckResult::degraded('Slow')->isOperational()); - $this->assertFalse(HealthCheckResult::unhealthy('Down')->isOperational()); - $this->assertFalse(HealthCheckResult::unknown()->isOperational()); - } - - #[Test] - public function it_converts_to_array(): void - { - $result = HealthCheckResult::healthy( - 'Service operational', - ['version' => '1.0'], - 12.34 - ); - - $array = $result->toArray(); - - $this->assertSame('healthy', $array['status']); - $this->assertSame('Service operational', $array['message']); - $this->assertSame(['version' => '1.0'], $array['data']); - $this->assertSame(12.34, $array['response_time_ms']); - } - - #[Test] - public function it_omits_null_values_in_array(): void - { - $result = HealthCheckResult::healthy(); - - $array = $result->toArray(); - - $this->assertArrayHasKey('status', $array); - $this->assertArrayHasKey('message', $array); - $this->assertArrayNotHasKey('data', $array); - $this->assertArrayNotHasKey('response_time_ms', $array); - } - - #[Test] - public function it_uses_default_healthy_message(): void - { - $result = HealthCheckResult::healthy(); - - $this->assertSame('Service is healthy', $result->message); - } -} diff --git a/src/Core/Service/Tests/Unit/ServiceStatusTest.php b/src/Core/Service/Tests/Unit/ServiceStatusTest.php deleted file mode 100644 index 561ff73..0000000 --- a/src/Core/Service/Tests/Unit/ServiceStatusTest.php +++ /dev/null @@ -1,102 +0,0 @@ -assertSame('healthy', ServiceStatus::HEALTHY->value); - $this->assertSame('degraded', ServiceStatus::DEGRADED->value); - $this->assertSame('unhealthy', ServiceStatus::UNHEALTHY->value); - $this->assertSame('unknown', ServiceStatus::UNKNOWN->value); - } - - #[Test] - public function it_checks_operational_status(): void - { - $this->assertTrue(ServiceStatus::HEALTHY->isOperational()); - $this->assertTrue(ServiceStatus::DEGRADED->isOperational()); - $this->assertFalse(ServiceStatus::UNHEALTHY->isOperational()); - $this->assertFalse(ServiceStatus::UNKNOWN->isOperational()); - } - - #[Test] - public function it_provides_human_readable_labels(): void - { - $this->assertSame('Healthy', ServiceStatus::HEALTHY->label()); - $this->assertSame('Degraded', ServiceStatus::DEGRADED->label()); - $this->assertSame('Unhealthy', ServiceStatus::UNHEALTHY->label()); - $this->assertSame('Unknown', ServiceStatus::UNKNOWN->label()); - } - - #[Test] - public function it_has_severity_ordering(): void - { - $this->assertLessThan( - ServiceStatus::DEGRADED->severity(), - ServiceStatus::HEALTHY->severity() - ); - - $this->assertLessThan( - ServiceStatus::UNHEALTHY->severity(), - ServiceStatus::DEGRADED->severity() - ); - - $this->assertLessThan( - ServiceStatus::UNKNOWN->severity(), - ServiceStatus::UNHEALTHY->severity() - ); - } - - #[Test] - public function it_creates_from_boolean(): void - { - $this->assertSame(ServiceStatus::HEALTHY, ServiceStatus::fromBoolean(true)); - $this->assertSame(ServiceStatus::UNHEALTHY, ServiceStatus::fromBoolean(false)); - } - - #[Test] - public function it_finds_worst_status(): void - { - $statuses = [ - ServiceStatus::HEALTHY, - ServiceStatus::DEGRADED, - ServiceStatus::HEALTHY, - ]; - - $this->assertSame(ServiceStatus::DEGRADED, ServiceStatus::worst($statuses)); - } - - #[Test] - public function it_returns_unknown_for_empty_array(): void - { - $this->assertSame(ServiceStatus::UNKNOWN, ServiceStatus::worst([])); - } - - #[Test] - public function it_finds_most_severe_status(): void - { - $statuses = [ - ServiceStatus::HEALTHY, - ServiceStatus::UNHEALTHY, - ServiceStatus::DEGRADED, - ]; - - $this->assertSame(ServiceStatus::UNHEALTHY, ServiceStatus::worst($statuses)); - } -} diff --git a/src/Core/Service/Tests/Unit/ServiceVersionTest.php b/src/Core/Service/Tests/Unit/ServiceVersionTest.php deleted file mode 100644 index d202fbe..0000000 --- a/src/Core/Service/Tests/Unit/ServiceVersionTest.php +++ /dev/null @@ -1,165 +0,0 @@ -assertSame(2, $version->major); - $this->assertSame(3, $version->minor); - $this->assertSame(4, $version->patch); - $this->assertFalse($version->deprecated); - $this->assertNull($version->deprecationMessage); - $this->assertNull($version->sunsetDate); - } - - #[Test] - public function it_creates_initial_version(): void - { - $version = ServiceVersion::initial(); - - $this->assertSame('1.0.0', $version->toString()); - } - - #[Test] - public function it_creates_version_from_string(): void - { - $version = ServiceVersion::fromString('2.3.4'); - - $this->assertSame(2, $version->major); - $this->assertSame(3, $version->minor); - $this->assertSame(4, $version->patch); - } - - #[Test] - public function it_handles_string_with_v_prefix(): void - { - $version = ServiceVersion::fromString('v1.2.3'); - - $this->assertSame('1.2.3', $version->toString()); - } - - #[Test] - public function it_handles_partial_version_string(): void - { - $version = ServiceVersion::fromString('2'); - - $this->assertSame(2, $version->major); - $this->assertSame(0, $version->minor); - $this->assertSame(0, $version->patch); - } - - #[Test] - public function it_marks_version_as_deprecated(): void - { - $version = new ServiceVersion(1, 0, 0); - $sunset = new \DateTimeImmutable('2025-06-01'); - - $deprecated = $version->deprecate('Use v2 instead', $sunset); - - $this->assertTrue($deprecated->deprecated); - $this->assertSame('Use v2 instead', $deprecated->deprecationMessage); - $this->assertEquals($sunset, $deprecated->sunsetDate); - // Original version should be unchanged - $this->assertFalse($version->deprecated); - } - - #[Test] - public function it_detects_past_sunset_date(): void - { - $version = (new ServiceVersion(1, 0, 0)) - ->deprecate('Old version', new \DateTimeImmutable('2020-01-01')); - - $this->assertTrue($version->isPastSunset()); - } - - #[Test] - public function it_detects_future_sunset_date(): void - { - $version = (new ServiceVersion(1, 0, 0)) - ->deprecate('Will be removed', new \DateTimeImmutable('2099-01-01')); - - $this->assertFalse($version->isPastSunset()); - } - - #[Test] - public function it_compares_versions_correctly(): void - { - $v1 = new ServiceVersion(1, 0, 0); - $v2 = new ServiceVersion(2, 0, 0); - $v1_1 = new ServiceVersion(1, 1, 0); - $v1_0_1 = new ServiceVersion(1, 0, 1); - - $this->assertSame(-1, $v1->compare($v2)); - $this->assertSame(1, $v2->compare($v1)); - $this->assertSame(0, $v1->compare(new ServiceVersion(1, 0, 0))); - $this->assertSame(-1, $v1->compare($v1_1)); - $this->assertSame(-1, $v1->compare($v1_0_1)); - } - - #[Test] - public function it_checks_compatibility(): void - { - $v2_3 = new ServiceVersion(2, 3, 0); - $v2_1 = new ServiceVersion(2, 1, 0); - $v2_5 = new ServiceVersion(2, 5, 0); - $v3_0 = new ServiceVersion(3, 0, 0); - - $this->assertTrue($v2_3->isCompatibleWith($v2_1)); - $this->assertFalse($v2_3->isCompatibleWith($v2_5)); - $this->assertFalse($v2_3->isCompatibleWith($v3_0)); - } - - #[Test] - public function it_converts_to_string(): void - { - $version = new ServiceVersion(1, 2, 3); - - $this->assertSame('1.2.3', $version->toString()); - $this->assertSame('1.2.3', (string) $version); - } - - #[Test] - public function it_converts_to_array(): void - { - $version = (new ServiceVersion(1, 2, 3)) - ->deprecate('Use v2', new \DateTimeImmutable('2025-06-01')); - - $array = $version->toArray(); - - $this->assertSame('1.2.3', $array['version']); - $this->assertTrue($array['deprecated']); - $this->assertSame('Use v2', $array['deprecation_message']); - $this->assertSame('2025-06-01', $array['sunset_date']); - } - - #[Test] - public function it_omits_null_values_in_array(): void - { - $version = new ServiceVersion(1, 0, 0); - - $array = $version->toArray(); - - $this->assertArrayHasKey('version', $array); - $this->assertArrayNotHasKey('deprecated', $array); - $this->assertArrayNotHasKey('deprecation_message', $array); - $this->assertArrayNotHasKey('sunset_date', $array); - } -} diff --git a/src/Website/Service/Boot.php b/src/Website/Service/Boot.php deleted file mode 100644 index f5a78e3..0000000 --- a/src/Website/Service/Boot.php +++ /dev/null @@ -1,34 +0,0 @@ -domain(request()->getHost()) - ->group(__DIR__.'/Routes/web.php'); - } -} diff --git a/src/Website/Service/Routes/web.php b/src/Website/Service/Routes/web.php deleted file mode 100644 index c8a68fb..0000000 --- a/src/Website/Service/Routes/web.php +++ /dev/null @@ -1,20 +0,0 @@ -name('service.home'); -Route::get('/features', Features::class)->name('service.features'); diff --git a/src/Website/Service/View/Blade/components/footer.blade.php b/src/Website/Service/View/Blade/components/footer.blade.php deleted file mode 100644 index a13fdd3..0000000 --- a/src/Website/Service/View/Blade/components/footer.blade.php +++ /dev/null @@ -1,34 +0,0 @@ -
- {{-- Footer gradient border --}} -
-
-
-
- @if(config('core.app.logo') && file_exists(public_path(config('core.app.logo')))) - {{ config('core.app.name', 'Service') }} - @else -
- -
- @endif - - © {{ date('Y') }} {{ config('core.app.name', $workspace['name'] ?? 'Service') }} - -
-
- @if(config('core.app.privacy_url')) - Privacy - @endif - @if(config('core.app.terms_url')) - Terms - @endif - @if(config('core.app.powered_by')) - - - Powered by {{ config('core.app.powered_by') }} - - @endif -
-
-
-
diff --git a/src/Website/Service/View/Blade/components/header.blade.php b/src/Website/Service/View/Blade/components/header.blade.php deleted file mode 100644 index 6e02b07..0000000 --- a/src/Website/Service/View/Blade/components/header.blade.php +++ /dev/null @@ -1,32 +0,0 @@ -
-
-
- {{-- Branding --}} - -
- -
- - {{ $workspace['name'] ?? 'Service' }} - -
- - {{-- Navigation --}} - - - {{-- Actions --}} - -
-
- {{-- Header gradient border --}} -
-
diff --git a/src/Website/Service/View/Blade/features.blade.php b/src/Website/Service/View/Blade/features.blade.php deleted file mode 100644 index 312a72e..0000000 --- a/src/Website/Service/View/Blade/features.blade.php +++ /dev/null @@ -1,92 +0,0 @@ -
- {{-- Hero Section --}} -
-
-
- {{-- Badge --}} -
- - {{ $workspace['name'] ?? 'Service' }} Features -
- - {{-- Headline --}} -

- Everything you need to - succeed -

- - {{-- Description --}} -

- {{ $workspace['description'] ?? 'Powerful features built for creators and businesses.' }} -

-
-
-
- - {{-- Features Grid --}} -
-
-
- @foreach($features as $feature) -
-
- -
-

{{ $feature['title'] }}

-

{{ $feature['description'] }}

-
- @endforeach -
-
-
- - {{-- Integration Section --}} -
-
-

- Part of the Host UK ecosystem -

-

- {{ $workspace['name'] ?? 'This service' }} works seamlessly with other Host UK services. - One account, unified billing, integrated tools. -

- -
-
- - {{-- CTA Section --}} -
-
-

- Ready to get started? -

-

- Start your free trial today. No credit card required. -

- -
-
-
diff --git a/src/Website/Service/View/Blade/landing.blade.php b/src/Website/Service/View/Blade/landing.blade.php deleted file mode 100644 index 10f34a0..0000000 --- a/src/Website/Service/View/Blade/landing.blade.php +++ /dev/null @@ -1,76 +0,0 @@ -
- {{-- Hero Section --}} -
-
-
- {{-- Badge --}} -
- - {{ $workspace['name'] ?? 'Service' }} -
- - {{-- Headline --}} -

- {{ $workspace['description'] ?? 'A powerful service' }} - built for creators -

- - {{-- Description --}} -

- {{ config('core.app.tagline', 'Simple, powerful tools that help you build your online presence.') }} -

- - {{-- CTAs --}} - -
-
-
- - {{-- Features Section --}} -
-
-
-

- Everything you need -

-

- Powerful tools built for creators and businesses. -

-
- -
- @foreach($features as $feature) -
-
- -
-

{{ $feature['title'] }}

-

{{ $feature['description'] }}

-
- @endforeach -
-
-
- - {{-- CTA Section --}} -
-
-

- Ready to get started? -

-

- {{ config('core.app.cta_text', 'Join thousands of users building with our platform.') }} -

- - Get started free - -
-
-
diff --git a/src/Website/Service/View/Blade/layouts/service.blade.php b/src/Website/Service/View/Blade/layouts/service.blade.php deleted file mode 100644 index 4eaa4a8..0000000 --- a/src/Website/Service/View/Blade/layouts/service.blade.php +++ /dev/null @@ -1,97 +0,0 @@ -@php - $color = $workspace['color'] ?? 'violet'; - $colorMap = [ - 'violet' => ['accent' => '#8b5cf6', 'hover' => '#a78bfa'], - 'green' => ['accent' => '#22c55e', 'hover' => '#4ade80'], - 'yellow' => ['accent' => '#eab308', 'hover' => '#facc15'], - 'orange' => ['accent' => '#f97316', 'hover' => '#fb923c'], - 'red' => ['accent' => '#ef4444', 'hover' => '#f87171'], - 'cyan' => ['accent' => '#06b6d4', 'hover' => '#22d3ee'], - 'blue' => ['accent' => '#3b82f6', 'hover' => '#60a5fa'], - ]; - $colors = $colorMap[$color] ?? $colorMap['violet']; -@endphp - - - - - - - - {{ $title ?? $workspace['name'] ?? config('core.app.name', 'Service') }} - - - - - - - - - @if(View::exists('layouts::partials.fonts')) - @include('layouts::partials.fonts') - @endif - @if(file_exists(public_path('vendor/fontawesome/css/all.min.css'))) - - @endif - @vite(['resources/css/app.css', 'resources/js/app.js']) - @if(class_exists(\Flux\Flux::class)) - @fluxAppearance - @endif - - - - - - {{-- Decorative background shapes --}} - - - {{-- Header --}} - @include('service::components.header', ['workspace' => $workspace, 'colors' => $colors]) - - {{-- Main Content --}} -
- {{-- Radial gradient glow at top of content --}} - - - {{ $slot }} -
- - {{-- Footer --}} - @include('service::components.footer', ['workspace' => $workspace, 'colors' => $colors]) - - @if(class_exists(\Flux\Flux::class)) - @fluxScripts - @endif - - {{ $scripts ?? '' }} - - @stack('scripts') - - diff --git a/src/Website/Service/View/Features.php b/src/Website/Service/View/Features.php deleted file mode 100644 index a626ddb..0000000 --- a/src/Website/Service/View/Features.php +++ /dev/null @@ -1,163 +0,0 @@ -getHost(); - $slug = $this->extractSubdomain($host); - - // Try to resolve workspace from app container if service exists - if ($slug && app()->bound('workspace.service')) { - $this->workspace = app('workspace.service')->get($slug) ?? []; - } - - // Fallback to app config defaults - if (empty($this->workspace)) { - $this->workspace = [ - 'name' => config('core.app.name', config('app.name', 'Service')), - 'slug' => 'service', - 'icon' => config('core.app.icon', 'cube'), - 'color' => config('core.app.color', 'violet'), - 'description' => config('core.app.description', 'A powerful platform'), - ]; - } - } - - /** - * Extract subdomain from hostname. - */ - protected function extractSubdomain(string $host): ?string - { - if (preg_match('/^([a-z]+)\.host\.(test|localhost|uk\.com)$/i', $host, $matches)) { - return $matches[1]; - } - - return null; - } - - /** - * Get detailed features for this service. - */ - public function getFeatures(): array - { - $slug = $this->workspace['slug'] ?? 'service'; - - // Service-specific features - return match ($slug) { - 'social' => $this->getSocialFeatures(), - 'analytics' => $this->getAnalyticsFeatures(), - 'notify' => $this->getNotifyFeatures(), - 'trust' => $this->getTrustFeatures(), - 'support' => $this->getSupportFeatures(), - default => $this->getDefaultFeatures(), - }; - } - - protected function getSocialFeatures(): array - { - return [ - ['icon' => 'calendar', 'title' => 'Schedule posts', 'description' => 'Plan your content calendar weeks in advance with our visual scheduler.'], - ['icon' => 'share-nodes', 'title' => 'Multi-platform publishing', 'description' => 'Publish to 20+ social networks from a single dashboard.'], - ['icon' => 'chart-pie', 'title' => 'Analytics & insights', 'description' => 'Track engagement, reach, and growth across all your accounts.'], - ['icon' => 'users', 'title' => 'Team collaboration', 'description' => 'Work together with approval workflows and role-based access.'], - ['icon' => 'wand-magic-sparkles', 'title' => 'AI content assistant', 'description' => 'Generate captions, hashtags, and post ideas with AI.'], - ['icon' => 'inbox', 'title' => 'Unified inbox', 'description' => 'Manage comments and messages from all platforms in one place.'], - ]; - } - - protected function getAnalyticsFeatures(): array - { - return [ - ['icon' => 'cookie-bite', 'title' => 'No cookies required', 'description' => 'Privacy-focused tracking without consent banners.'], - ['icon' => 'bolt', 'title' => 'Lightweight script', 'description' => 'Under 1KB script that won\'t slow down your site.'], - ['icon' => 'chart-line', 'title' => 'Real-time dashboard', 'description' => 'See visitors on your site as they browse.'], - ['icon' => 'route', 'title' => 'Goal tracking', 'description' => 'Set up funnels and conversion goals to measure success.'], - ['icon' => 'flask', 'title' => 'A/B testing', 'description' => 'Test variations and measure statistical significance.'], - ['icon' => 'file-export', 'title' => 'Data export', 'description' => 'Export your data anytime in CSV or JSON format.'], - ]; - } - - protected function getNotifyFeatures(): array - { - return [ - ['icon' => 'bell', 'title' => 'Web push notifications', 'description' => 'Reach subscribers directly in their browser.'], - ['icon' => 'users-gear', 'title' => 'Audience segments', 'description' => 'Target specific groups based on behaviour and preferences.'], - ['icon' => 'diagram-project', 'title' => 'Automation flows', 'description' => 'Create drip campaigns triggered by user actions.'], - ['icon' => 'vials', 'title' => 'A/B testing', 'description' => 'Test different messages to optimise engagement.'], - ['icon' => 'chart-simple', 'title' => 'Delivery analytics', 'description' => 'Track delivery, clicks, and conversion rates.'], - ['icon' => 'clock', 'title' => 'Scheduled sends', 'description' => 'Schedule notifications for optimal delivery times.'], - ]; - } - - protected function getTrustFeatures(): array - { - return [ - ['icon' => 'comment-dots', 'title' => 'Social proof popups', 'description' => 'Show real-time purchase and signup notifications.'], - ['icon' => 'star', 'title' => 'Review collection', 'description' => 'Collect and display customer reviews automatically.'], - ['icon' => 'bullseye', 'title' => 'Smart targeting', 'description' => 'Show notifications to the right visitors at the right time.'], - ['icon' => 'palette', 'title' => 'Custom styling', 'description' => 'Match notifications to your brand with full CSS control.'], - ['icon' => 'chart-column', 'title' => 'Conversion tracking', 'description' => 'Measure the impact on your conversion rates.'], - ['icon' => 'plug', 'title' => 'Easy integration', 'description' => 'Add to any website with a single script tag.'], - ]; - } - - protected function getSupportFeatures(): array - { - return [ - ['icon' => 'inbox', 'title' => 'Shared inbox', 'description' => 'Manage customer emails from a unified team inbox.'], - ['icon' => 'book', 'title' => 'Help centre', 'description' => 'Build a self-service knowledge base for customers.'], - ['icon' => 'comments', 'title' => 'Live chat widget', 'description' => 'Embed a chat widget on your website for instant support.'], - ['icon' => 'clock-rotate-left', 'title' => 'SLA tracking', 'description' => 'Set response time targets and track performance.'], - ['icon' => 'reply', 'title' => 'Canned responses', 'description' => 'Save time with pre-written replies for common questions.'], - ['icon' => 'tags', 'title' => 'Ticket management', 'description' => 'Organise conversations with tags and custom fields.'], - ]; - } - - protected function getDefaultFeatures(): array - { - return [ - ['icon' => 'rocket', 'title' => 'Easy to use', 'description' => 'Get started in minutes with our intuitive interface.'], - ['icon' => 'shield-check', 'title' => 'Secure by default', 'description' => 'Built with security and privacy at the core.'], - ['icon' => 'chart-line', 'title' => 'Analytics included', 'description' => 'Track performance with built-in analytics.'], - ['icon' => 'puzzle-piece', 'title' => 'Modular architecture', 'description' => 'Extend with modules to fit your exact needs.'], - ['icon' => 'headset', 'title' => 'UK-based support', 'description' => 'Get help from our friendly support team.'], - ['icon' => 'code', 'title' => 'Developer friendly', 'description' => 'Full API access and webhook integrations.'], - ]; - } - - public function render(): View - { - $appName = config('core.app.name', config('app.name', 'Service')); - - return view('service::features', [ - 'workspace' => $this->workspace, - 'features' => $this->getFeatures(), - ])->layout('service::layouts.service', [ - 'title' => 'Features - '.($this->workspace['name'] ?? $appName), - 'workspace' => $this->workspace, - ]); - } -} diff --git a/src/Website/Service/View/Landing.php b/src/Website/Service/View/Landing.php deleted file mode 100644 index 455aa72..0000000 --- a/src/Website/Service/View/Landing.php +++ /dev/null @@ -1,100 +0,0 @@ -getHost(); - $slug = $this->extractSubdomain($host); - - // Try to resolve workspace from app container if service exists - if ($slug && app()->bound('workspace.service')) { - $this->workspace = app('workspace.service')->get($slug) ?? []; - } - - // Fallback to app config defaults - if (empty($this->workspace)) { - $this->workspace = [ - 'name' => config('core.app.name', config('app.name', 'Service')), - 'slug' => 'service', - 'icon' => config('core.app.icon', 'cube'), - 'color' => config('core.app.color', 'violet'), - 'description' => config('core.app.description', 'A powerful platform'), - ]; - } - } - - /** - * Extract subdomain from hostname. - */ - protected function extractSubdomain(string $host): ?string - { - // Handle patterns like social.host.test, social.host.uk.com - if (preg_match('/^([a-z]+)\.host\.(test|localhost|uk\.com)$/i', $host, $matches)) { - return $matches[1]; - } - - return null; - } - - /** - * Get features for the service. - */ - public function getFeatures(): array - { - // Generic features - services can override in their own Landing - return [ - [ - 'icon' => 'rocket', - 'title' => 'Easy to use', - 'description' => 'Get started in minutes with our intuitive interface.', - ], - [ - 'icon' => 'shield-check', - 'title' => 'Secure by default', - 'description' => 'Built with security and privacy at the core.', - ], - [ - 'icon' => 'chart-line', - 'title' => 'Analytics included', - 'description' => 'Track performance with built-in analytics.', - ], - [ - 'icon' => 'puzzle-piece', - 'title' => 'Modular architecture', - 'description' => 'Extend with modules to fit your exact needs.', - ], - ]; - } - - public function render(): View - { - $appName = config('core.app.name', config('app.name', 'Service')); - - return view('service::landing', [ - 'workspace' => $this->workspace, - 'features' => $this->getFeatures(), - ])->layout('service::layouts.service', [ - 'title' => $this->workspace['name'] !== $appName - ? $this->workspace['name'].' - '.$appName - : $appName, - 'workspace' => $this->workspace, - ]); - } -}