Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.77% covered (success)
96.77%
30 / 31
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Database
96.77% covered (success)
96.77%
30 / 31
75.00% covered (warning)
75.00%
3 / 4
10
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 factory
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getInstance
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createTenantDatabase
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3/**
4 * The phpMyFAQ instance basic database class.
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2015-2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2015-02-14
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Instance;
21
22use phpMyFAQ\Configuration;
23use phpMyFAQ\Core\Exception;
24use phpMyFAQ\Instance\Database\DriverInterface;
25use phpMyFAQ\Setup\Installation\SchemaInstaller;
26use phpMyFAQ\Setup\Migration\QueryBuilder\DialectFactory;
27
28/**
29 * Class Database
30 *
31 * @package phpMyFAQ\Instance
32 */
33class Database
34{
35    /**
36     * Instance.
37     */
38    private static ?DriverInterface $driver = null;
39
40    /**
41     * Constructor.
42     */
43    private function __construct(
44        protected Configuration $configuration,
45    ) {
46    }
47
48    /**
49     * Database factory.
50     *
51     * Returns a SchemaInstaller that uses the dialect-agnostic DatabaseSchema.
52     *
53     * @param Configuration $configuration phpMyFAQ configuration container
54     * @param string        $type Database management system type
55     * @throws Exception
56     */
57    public static function factory(Configuration $configuration, string $type): DriverInterface
58    {
59        try {
60            $dialect = DialectFactory::createForType(strtolower($type));
61        } catch (\InvalidArgumentException) {
62            throw new Exception('Invalid Database Type: ' . $type);
63        }
64
65        self::$driver = new SchemaInstaller($configuration, $dialect);
66
67        return self::$driver;
68    }
69
70    /**
71     * Returns the single instance.
72     */
73    public static function getInstance(): ?DriverInterface
74    {
75        return self::$driver;
76    }
77
78    /**
79     * Creates a dedicated tenant database for supported drivers (PostgreSQL, SQL Server).
80     *
81     * @throws \RuntimeException if the driver does not support database-per-tenant isolation
82     */
83    public static function createTenantDatabase(Configuration $configuration, string $type, string $databaseName): bool
84    {
85        $normalizedType = strtolower($type);
86
87        if (!preg_match('/^[A-Za-z0-9_]+$/', $databaseName)) {
88            throw new \InvalidArgumentException(sprintf('Invalid tenant database identifier: "%s".', $databaseName));
89        }
90
91        if (str_contains($normalizedType, 'pgsql')) {
92            $existsQuery = sprintf(
93                "SELECT 1 FROM pg_database WHERE datname = '%s'",
94                $configuration->getDb()->escape($databaseName),
95            );
96            $existsResult = $configuration->getDb()->query($existsQuery);
97            if ($existsResult !== false && $configuration->getDb()->numRows($existsResult) > 0) {
98                return true;
99            }
100
101            return (bool) $configuration->getDb()->query(sprintf('CREATE DATABASE "%s"', $databaseName));
102        }
103
104        if (str_contains($normalizedType, 'sqlsrv')) {
105            return (bool) $configuration
106                ->getDb()
107                ->query(sprintf(
108                    "IF DB_ID('%s') IS NULL CREATE DATABASE [%s]",
109                    $configuration->getDb()->escape($databaseName),
110                    $databaseName,
111                ));
112        }
113
114        throw new \RuntimeException(sprintf(
115            'Database-per-tenant isolation is not supported for driver "%s". Use PostgreSQL or SQL Server.',
116            $type,
117        ));
118    }
119}