2017-07-15 11:52:34 -05:00
< ? php
2024-03-12 22:39:16 -04:00
namespace App\Services\Databases ;
2017-07-15 11:52:34 -05:00
2025-09-24 13:34:19 +02:00
use App\Exceptions\Repository\DuplicateDatabaseNameException ;
use App\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException ;
use App\Exceptions\Service\Database\TooManyDatabasesException ;
2025-03-06 09:28:45 -05:00
use App\Facades\Activity ;
2024-03-12 22:39:16 -04:00
use App\Helpers\Utilities ;
2025-09-24 13:34:19 +02:00
use App\Models\Database ;
use App\Models\Server ;
use Exception ;
2020-06-23 20:07:37 -07:00
use Illuminate\Database\ConnectionInterface ;
2025-09-06 16:47:54 -04:00
use Illuminate\Support\Str ;
2025-09-24 13:34:19 +02:00
use InvalidArgumentException ;
use Throwable ;
2017-07-15 11:52:34 -05:00
2017-07-24 21:34:10 -05:00
class DatabaseManagementService
2017-07-15 11:52:34 -05:00
{
2020-10-11 11:59:46 -07:00
/**
* The regex used to validate that the database name passed through to the function is
* in the expected format .
*
2026-03-17 09:09:01 +01:00
* @ see DatabaseManagementService :: generateUniqueDatabaseName ()
2020-10-11 11:59:46 -07:00
*/
private const MATCH_NAME_REGEX = '/^(s[\d]+_)(.*)$/' ;
2018-03-01 21:27:37 -06:00
/**
2020-06-23 20:07:37 -07:00
* Determines if the service should validate the user ' s ability to create an additional
* database for this server . In almost all cases this should be true , but to keep things
* flexible you can also set it to false and create more databases than the server is
* allocated .
2018-03-01 21:27:37 -06:00
*/
2022-10-14 10:59:20 -06:00
protected bool $validateDatabaseLimit = true ;
2017-07-15 11:52:34 -05:00
public function __construct (
2022-10-14 10:59:20 -06:00
protected ConnectionInterface $connection ,
2024-11-22 09:27:57 +01:00
) {}
2017-07-15 11:52:34 -05:00
2020-10-11 11:59:46 -07:00
/**
* Generates a unique database name for the given server . This name should be passed through when
* calling this handle function for this service , otherwise the database will be created with
* whatever name is provided .
*/
public static function generateUniqueDatabaseName ( string $name , int $serverId ) : string
{
// Max of 48 characters, including the s123_ that we append to the front.
return sprintf ( 's%d_%s' , $serverId , substr ( $name , 0 , 48 - strlen ( " s { $serverId } _ " )));
}
2020-06-23 20:07:37 -07:00
/**
2022-10-14 10:59:20 -06:00
* Set whether this class should validate that the server has enough slots
2020-06-23 20:07:37 -07:00
* left before creating the new database .
*/
public function setValidateDatabaseLimit ( bool $validate ) : self
{
$this -> validateDatabaseLimit = $validate ;
return $this ;
}
2017-07-15 11:52:34 -05:00
/**
* Create a new database that is linked to a specific host .
*
2025-03-03 14:41:19 -05:00
* @ param array { database ? : string , database_host_id : int } $data
*
2025-09-08 13:12:33 -04:00
* @ throws Throwable
* @ throws TooManyDatabasesException
* @ throws DatabaseClientFeatureNotEnabledException
2017-07-15 11:52:34 -05:00
*/
2022-10-14 10:59:20 -06:00
public function create ( Server $server , array $data ) : Database
2017-07-15 11:52:34 -05:00
{
2026-07-09 09:55:11 -04:00
throw_unless ( config ( 'panel.client_features.databases.enabled' ), new DatabaseClientFeatureNotEnabledException ());
2020-06-23 20:07:37 -07:00
if ( $this -> validateDatabaseLimit ) {
// If the server has a limit assigned and we've already reached that limit, throw back
// an exception and kill the process.
2026-07-09 09:55:11 -04:00
throw_if ( ! is_null ( $server -> database_limit ) && $server -> databases () -> count () >= $server -> database_limit , new TooManyDatabasesException ());
2020-06-23 20:07:37 -07:00
}
2020-10-11 11:59:46 -07:00
// Protect against developer mistakes...
2026-07-09 09:55:11 -04:00
throw_if ( empty ( $data [ 'database' ]) || ! preg_match ( self :: MATCH_NAME_REGEX , $data [ 'database' ]), new InvalidArgumentException ( 'The database name passed to DatabaseManagementService::handle MUST be prefixed with "s{server_id}_".' ));
2020-09-24 19:31:42 -07:00
2020-06-23 20:07:37 -07:00
$data = array_merge ( $data , [
'server_id' => $server -> id ,
2025-09-06 16:47:54 -04:00
'username' => sprintf ( 'u%d_%s' , $server -> id , Str :: random ( 10 )),
2024-05-28 15:24:20 +02:00
'password' => Utilities :: randomStringWithSpecialCharacters ( 24 ),
2020-06-23 20:07:37 -07:00
]);
2024-11-13 17:05:48 -05:00
return $this -> connection -> transaction ( function () use ( $data ) {
2024-03-17 13:16:39 -04:00
$database = $this -> createModel ( $data );
2025-09-06 22:57:11 +02:00
$database
-> createDatabase ()
-> createUser ()
-> assignUserToDatabase ()
-> flushPrivileges ();
2024-03-17 13:16:39 -04:00
2025-03-06 09:28:45 -05:00
Activity :: event ( 'server:database.create' )
-> subject ( $database )
-> property ( 'name' , $database -> database )
-> log ();
2024-03-17 13:16:39 -04:00
return $database ;
});
2017-07-15 11:52:34 -05:00
}
/**
* Delete a database from the given host server .
*
2025-09-08 13:12:33 -04:00
* @ throws Throwable
2017-07-15 11:52:34 -05:00
*/
2022-10-14 10:59:20 -06:00
public function delete ( Database $database ) : ? bool
2017-07-15 11:52:34 -05:00
{
2025-04-29 17:05:49 +02:00
return $this -> connection -> transaction ( function () use ( $database ) {
2025-09-06 22:57:11 +02:00
$database
-> dropDatabase ()
-> dropUser ()
-> flushPrivileges ();
2025-04-29 17:05:49 +02:00
Activity :: event ( 'server:database.delete' )
-> subject ( $database )
-> property ( 'name' , $database -> database )
-> log ();
2017-07-15 11:52:34 -05:00
2025-04-29 17:05:49 +02:00
return $database -> delete ();
});
2017-07-15 11:52:34 -05:00
}
2020-10-11 11:59:46 -07:00
2025-09-06 22:57:11 +02:00
/**
* Updates a password for a given database .
*
2026-03-17 09:09:01 +01:00
* @ throws Exception
2025-09-06 22:57:11 +02:00
*/
public function rotatePassword ( Database $database ) : void
{
$password = Utilities :: randomStringWithSpecialCharacters ( 24 );
$this -> connection -> transaction ( function () use ( $database , $password ) {
$database -> update ([
'password' => $password ,
]);
$database
-> dropUser ()
-> createUser ()
-> assignUserToDatabase ()
-> flushPrivileges ();
});
}
2020-10-11 11:59:46 -07:00
/**
* Create the database if there is not an identical match in the DB . While you can technically
* have the same name across multiple hosts , for the sake of keeping this logic easy to understand
* and avoiding user confusion we will ignore the specific host and just look across all hosts .
*
2025-03-03 14:41:19 -05:00
* @ param array { server_id : int , database : string } $data
*
2025-09-08 13:12:33 -04:00
* @ throws DuplicateDatabaseNameException
* @ throws Throwable
2020-10-11 11:59:46 -07:00
*/
protected function createModel ( array $data ) : Database
{
$exists = Database :: query () -> where ( 'server_id' , $data [ 'server_id' ])
-> where ( 'database' , $data [ 'database' ])
-> exists ();
2026-07-09 09:55:11 -04:00
throw_if ( $exists , new DuplicateDatabaseNameException ( 'A database with that name already exists for this server.' ));
2020-10-11 11:59:46 -07:00
2021-01-23 12:33:34 -08:00
$database = ( new Database ()) -> forceFill ( $data );
2020-10-11 11:59:46 -07:00
$database -> saveOrFail ();
return $database ;
}
2017-07-15 11:52:34 -05:00
}