Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.43% covered (success)
96.43%
54 / 56
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ProfileManager
96.36% covered (success)
96.36%
53 / 55
85.71% covered (warning)
85.71%
6 / 7
26
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resolveActiveProfile
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
 resolveFromEnvironment
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 detectProfileFromHostOrPath
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
5.58
 resolveFromDatabase
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
7
 loadDiDefinitions
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 loadParameters
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Profile\Application\Service;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Profile\Domain\Model\Profile;
12use App\Core\Profile\Domain\Repository\ProfileRegistryInterface;
13use PDO;
14
15/**
16 * Profile Manager Application Service.
17 *
18 * Coordinates active profile detection, DI container configuration loading, and template path resolution.
19 *
20 * @package App\Core\Profile\Application\Service
21 */
22final readonly class ProfileManager
23{
24    /**
25     * ProfileManager constructor.
26     *
27     * @param ProfileRegistryInterface $registry Registry of available profiles.
28     * @param PDO|null                 $pdo      Optional database connection for setting resolution.
29     * @param string                   $prefix   Database table prefix.
30     */
31    public function __construct(
32        private ProfileRegistryInterface $registry,
33        private ?PDO $pdo = null,
34        private string $prefix = 'a_'
35    ) {
36    }
37
38    /**
39     * Resolves the active application profile.
40     *
41     * Priority:
42     * 1. Environment / Server variable `APP_PROFILE`
43     * 2. Database setting `app_profile` or `app_name`
44     * 3. Default fallback: 'admin'
45     *
46     * @return Profile Active profile instance.
47     */
48    public function resolveActiveProfile(): Profile
49    {
50        $fromEnv = $this->resolveFromEnvironment();
51        if ($fromEnv !== null) {
52            return $fromEnv;
53        }
54
55        $fromDb = $this->resolveFromDatabase();
56        if ($fromDb !== null) {
57            return $fromDb;
58        }
59
60        return $this->registry->get('admin') ?? new Profile(
61            name: 'admin',
62            label: 'Admin',
63            basePath: '',
64            sqlPath: '',
65            configPath: '',
66            viewsPath: '',
67            srcPath: ''
68        );
69    }
70
71    /**
72     * Attempts to resolve profile from environment variables, hostnames, or paths.
73     */
74    private function resolveFromEnvironment(): ?Profile
75    {
76        $envProfile = $_ENV['APP_PROFILE'] ?? $_SERVER['APP_PROFILE'] ?? null;
77        if (is_string($envProfile) && $envProfile !== '') {
78            return $this->registry->get($envProfile);
79        }
80
81        $detectedName = $this->detectProfileFromHostOrPath();
82
83        return $detectedName !== null ? $this->registry->get($detectedName) : null;
84    }
85
86    private function detectProfileFromHostOrPath(): ?string
87    {
88        $host = (string) ($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? '');
89        $scriptPath = (string) ($_SERVER['SCRIPT_FILENAME'] ?? $_SERVER['DOCUMENT_ROOT'] ?? __DIR__);
90
91        if (str_contains($host, 'app-admin') || str_contains($scriptPath, 'app-admin')) {
92            return 'admin';
93        }
94        if (str_contains($host, 'app-client') || str_contains($scriptPath, 'app-client')) {
95            return 'client';
96        }
97
98        return null;
99    }
100
101    /**
102     * Attempts to resolve profile from database settings.
103     */
104    private function resolveFromDatabase(): ?Profile
105    {
106        if ($this->pdo === null) {
107            return null;
108        }
109        $resolved = null;
110        try {
111            $stmt = $this->pdo->prepare(
112                "SELECT `setting_key`, `setting_value` FROM `{$this->prefix}core_settings_records` " .
113                "WHERE `setting_key` IN ('app_profile', 'app_name')"
114            );
115            $stmt->execute();
116            /** @var array<string, string> $settings */
117            $settings = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
118
119            if (!empty($settings['app_profile'])) {
120                $resolved = $this->registry->get($settings['app_profile']);
121            }
122            if (
123                $resolved === null
124                && !empty($settings['app_name'])
125                && stripos($settings['app_name'], 'client') !== false
126            ) {
127                $resolved = $this->registry->get('client');
128            }
129        } catch (\Throwable) {
130            // Database query error ignored, fallback to default
131        }
132        return $resolved;
133    }
134
135    /**
136     * Loads DI container definitions for the active profile.
137     *
138     * @param Profile $profile Profile instance.
139     * @return array<string, mixed> Profile DI definitions.
140     */
141    public function loadDiDefinitions(Profile $profile): array
142    {
143        if (!$profile->hasDiConfig()) {
144            return [];
145        }
146
147        $diFile = $profile->configPath . '/di.php';
148        /** @var mixed $definitions */
149        $definitions = (static fn(string $f): mixed => require_once $f)($diFile);
150
151        return is_array($definitions) ? $definitions : [];
152    }
153
154    /**
155     * Loads parameters configuration for the active profile.
156     *
157     * @param Profile $profile Profile instance.
158     * @return array<string, mixed> Profile parameters.
159     */
160    public function loadParameters(Profile $profile): array
161    {
162        if (!$profile->hasParamsConfig()) {
163            return [];
164        }
165
166        $paramsFile = $profile->configPath . '/params.php';
167        /** @var mixed $params */
168        $params = (static fn(string $f): mixed => require_once $f)($paramsFile);
169
170        return is_array($params) ? $params : [];
171    }
172}