Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Database Connection Helper Command #122

Merged
merged 13 commits into from
Feb 11, 2020
24 changes: 24 additions & 0 deletions docs/database.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Interacting with the Database

You can log into MySQL and run any query in the default database (`wordpress`) by using:

```sh
composer server db
```

Use `composer server db info` to retrieve MySQL info and connection details:

```sh
Root password: wordpress

Database: wordpress
User: wordpress
Password: wordpress

Host: 0.0.0.0
Port: 32775

Version: 5.7.26-1debian9
igmoweb marked this conversation as resolved.
Show resolved Hide resolved
```

Use `composer server db sequel` to open the database in Sequel Pro. This command can only be run under MacOS and requires [Sequel Pro](https://www.sequelpro.com/) to be installed on your computer.
139 changes: 134 additions & 5 deletions inc/composer/class-command.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ protected function configure() {
->setName( 'server' )
->setDescription( 'Altis Local Server' )
->setDefinition( [
new InputArgument( 'subcommand', null, 'start, stop, restart, cli, exec. shell, status. logs.' ),
new InputArgument( 'subcommand', null, 'start, stop, restart, cli, exec, shell, status, db, logs.' ),
new InputArgument( 'options', InputArgument::IS_ARRAY ),
] )
->setAliases( [ 'local-server' ] )
Expand All @@ -42,6 +42,10 @@ protected function configure() {
exec -- <command> eg: exec -- vendor/bin/phpcs
Open a shell:
shell
Database commands:
db Log into MySQL on the Database server
db sequel Generates an SPF file for Sequel Pro
db info Prints out Database connection details
View the logs
logs <service> <service> can be php, nginx, db, s3, elasticsearch, xray
EOT
Expand All @@ -53,6 +57,15 @@ public function isProxyCommand() {
return true;
}

private function get_base_command_prefix() {
return sprintf(
'cd %s; VOLUME=%s COMPOSE_PROJECT_NAME=%s',
'vendor/altis/local-server/docker',
escapeshellarg( getcwd() ),
$this->get_project_subdomain()
);
}

protected function execute( InputInterface $input, OutputInterface $output ) {
$subcommand = $input->getArgument( 'subcommand' );

Expand All @@ -68,6 +81,8 @@ protected function execute( InputInterface $input, OutputInterface $output ) {
return $this->exec( $input, $output, 'wp' );
} elseif ( $subcommand === 'exec' ) {
return $this->exec( $input, $output );
} elseif ( $subcommand === 'db' ) {
return $this->db( $input, $output );
} elseif ( $subcommand === 'status' ) {
return $this->status( $input, $output );
} elseif ( $subcommand === 'logs' ) {
Expand Down Expand Up @@ -320,18 +335,132 @@ protected function logs( InputInterface $input, OutputInterface $output ) {
protected function shell( InputInterface $input, OutputInterface $output ) {
$columns = exec( 'tput cols' );
$lines = exec( 'tput lines' );
$command_prefix = $this->get_base_command_prefix();
passthru( sprintf(
'cd %s; VOLUME=%s COMPOSE_PROJECT_NAME=%s docker-compose exec -e COLUMNS=%d -e LINES=%d php /bin/bash',
'vendor/altis/local-server/docker',
escapeshellarg( getcwd() ),
$this->get_project_subdomain(),
"$command_prefix docker-compose exec -e COLUMNS=%d -e LINES=%d php /bin/bash",
$columns,
$lines
), $return_val );

return $return_val;
}

protected function db( InputInterface $input, OutputInterface $output ) {
$db = $input->getArgument( 'options' )[0] ?? null;
$columns = exec( 'tput cols' );
$lines = exec( 'tput lines' );

$base_command_prefix = $this->get_base_command_prefix();

$base_command = sprintf(
"$base_command_prefix docker-compose exec -e COLUMNS=%d -e LINES=%d db",
$columns,
$lines
);

$return_val = 0;

switch ( $db ) {
case 'info':
$connection_data = $this->get_db_connection_data();

$db_info = <<<EOT
<info>Root password</info>: ${connection_data['MYSQL_ROOT_PASSWORD']}

<info>Database</info>: ${connection_data['MYSQL_DATABASE']}
<info>User</info>: ${connection_data['MYSQL_USER']}
<info>Password</info>: ${connection_data['MYSQL_PASSWORD']}

<info>Host</info>: ${connection_data['HOST']}
<info>Port</info>: ${connection_data['PORT']}

<comment>Version</comment>: ${connection_data['MYSQL_VERSION']}

EOT;
$output->write( $db_info );
break;
case 'sequel':
if ( strpos( php_uname(), 'Darwin' ) === false ) {
$output->writeln( '<error>This command is only supported on MacOS, use composer server db info to see the database connection details.</error>' );
return 1;
}

$connection_data = $this->get_db_connection_data();
$spf_file_contents = file_get_contents( dirname( __DIR__, 2 ) . '/templates/sequel.xml' );
foreach ( $connection_data as $field_name => $field_value ) {
$spf_file_contents = preg_replace( "/(<%=\s)($field_name)(\s%>)/i", $field_value, $spf_file_contents );
}
$output_file_path = sprintf( '/tmp/%s.spf', $this->get_project_subdomain() );
file_put_contents( $output_file_path, $spf_file_contents );

exec( "open $output_file_path", $null, $return_val );
if ( $return_val !== 0 ) {
$output->writeln( '<error>You must have Sequel Pro (https://www.sequelpro.com) installed to use this command</error>' );
}

break;
case null:
passthru( "$base_command mysql --database=wordpress --user=root -pwordpress", $return_val );
break;
default:
$output->writeln( "<error>The subcommand $db is not recognized</error>" );
Copy link
Contributor

@roborourke roborourke Feb 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$output->writeln( "<error>The subcommand $db is not recognized</error>" );
$output->writeln( "<error>The subcommand $db is not recognized</error>" );
return 1;

Adding a non zero exit code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've assigned 1 to $return_val instead. Although we already have one early return, it can get confusing if we write more code in that function.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good shout, makes sense to me

$return_val = 1;
}

return $return_val;
}

/**
* Return the Database connection details.
*
* @return array
*/
private function get_db_connection_data() {
$command_prefix = $this->get_base_command_prefix();
$columns = exec( 'tput cols' );
$lines = exec( 'tput lines' );

$base_command = sprintf(
"$command_prefix docker-compose exec -e COLUMNS=%d -e LINES=%d db",
$columns,
$lines
);

// Env variables
$env_variables = preg_split( '/\r\n|\r|\n/', shell_exec( "$base_command printenv" ) );
$values = array_reduce( $env_variables, function( $values, $env_variable_text ) {
$env_variable = explode( '=', $env_variable_text );
$values[ $env_variable[0] ] = $env_variable[1] ?? '';
return $values;
}, [] );

$keys = [
'MYSQL_ROOT_PASSWORD',
'MYSQL_PASSWORD',
'MYSQL_USER',
'MYSQL_DATABASE',
'MYSQL_VERSION',
];

array_walk( $values, function ( $value, $key ) use ( $keys ) {
return in_array( $key, $keys, true ) ? $value : false;
} );

$db_container_id = shell_exec( "$command_prefix docker-compose ps -q db" );

// Retrieve the forwarded ports using Docker and the container ID
$ports = shell_exec( sprintf( "$command_prefix docker ps --format '{{.Ports}}' --filter id=%s", $db_container_id ) );
preg_match( '/.*,\s([\d.]+):([\d]+)->.*/', $ports, $ports_matches );

return array_merge(
array_filter( $values ),
[
'HOST' => $ports_matches[1],
'PORT' => $ports_matches[2],
]
);
}

/**
* Get the name of the project for the local subdomain
*
Expand Down
72 changes: 72 additions & 0 deletions templates/sequel.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ContentFilters</key>
<dict/>
<key>auto_connect</key>
<true/>
<key>data</key>
<dict>
<key>connection</key>
<dict>
<key>database</key>
<string><%= MYSQL_DATABASE %></string>
<key>host</key>
<string><%= HOST %></string>
<key>name</key>
<string><%= MYSQL_USER %>@<%= HOST %></string>
<key>password</key>
<string><%= MYSQL_PASSWORD %></string>
<key>port</key>
<integer><%= PORT %></integer>
<key>rdbms_type</key>
<string>mysql</string>
<key>ssh_host</key>
<string></string>
<key>ssh_user</key>
<string></string>
<key>ssh_port</key>
<string></string>
<key>ssh_keyLocation</key>
<string></string>
<key>ssh_keyLocationEnabled</key>
<integer>0</integer>
<key>sslCACertFileLocation</key>
<string></string>
<key>sslCACertFileLocationEnabled</key>
<integer>0</integer>
<key>sslCertificateFileLocation</key>
<string></string>
<key>sslCertificateFileLocationEnabled</key>
<integer>0</integer>
<key>sslKeyFileLocation</key>
<string></string>
<key>sslKeyFileLocationEnabled</key>
<integer>0</integer>
<key>type</key>
<string>StandardConnection</string>
<key>useSSL</key>
<integer>0</integer>
<key>user</key>
<string><%= MYSQL_USER %></string>
</dict>
<key>session</key>
<dict/>
</dict>
<key>encrypted</key>
<false/>
<key>format</key>
<string>connection</string>
<key>queryFavorites</key>
<array/>
<key>queryHistory</key>
<array/>
<key>rdbms_type</key>
<string>mysql</string>
<key>rdbms_version</key>
<string><%= MYSQL_VERSION %></string>
<key>version</key>
<integer>1</integer>
</dict>
</plist>