diff --git a/.env b/.env index b872e8d..05ae2b3 100644 --- a/.env +++ b/.env @@ -2,5 +2,5 @@ APP_SERVICE=app DB_HOST=laravel-protector-mysql_testing-1 DB_DATABASE=protector_test -DB_USERNAME=forge -DB_PASSWORD=forge +DB_USERNAME=protector +DB_PASSWORD=protector diff --git a/docker-compose.yml b/compose.yml similarity index 96% rename from docker-compose.yml rename to compose.yml index 3f46631..d7c7fcf 100644 --- a/docker-compose.yml +++ b/compose.yml @@ -19,6 +19,7 @@ services: - '.:/var/www/html' networks: - internal + - shared depends_on: - mysql_testing mysql_testing: @@ -39,3 +40,5 @@ services: networks: internal: internal: true + shared: + external: true diff --git a/phpunit.xml.dist b/phpunit.xml.dist index dc3e135..d7e3dde 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -24,8 +24,8 @@ - - + + diff --git a/sail b/sail new file mode 100755 index 0000000..d9685f0 --- /dev/null +++ b/sail @@ -0,0 +1,603 @@ +#!/usr/bin/env bash + +UNAMEOUT="$(uname -s)" + +# Verify operating system is supported... +case "${UNAMEOUT}" in + Linux*) MACHINE=linux;; + Darwin*) MACHINE=mac;; + *) MACHINE="UNKNOWN" +esac + +if [ "$MACHINE" == "UNKNOWN" ]; then + echo "Unsupported operating system [$(uname -s)]. Laravel Sail supports macOS, Linux, and Windows (WSL2)." >&2 + + exit 1 +fi + +# Determine if stdout is a terminal... +if test -t 1; then + # Determine if colors are supported... + ncolors=$(tput colors) + + if test -n "$ncolors" && test "$ncolors" -ge 8; then + BOLD="$(tput bold)" + YELLOW="$(tput setaf 3)" + GREEN="$(tput setaf 2)" + NC="$(tput sgr0)" + fi +fi + +# Function that prints the available commands... +function display_help { + echo "Laravel Sail" + echo + echo "${YELLOW}Usage:${NC}" >&2 + echo " sail COMMAND [options] [arguments]" + echo + echo "Unknown commands are passed to the docker-compose binary." + echo + echo "${YELLOW}docker-compose Commands:${NC}" + echo " ${GREEN}sail up${NC} Start the application" + echo " ${GREEN}sail up -d${NC} Start the application in the background" + echo " ${GREEN}sail stop${NC} Stop the application" + echo " ${GREEN}sail restart${NC} Restart the application" + echo " ${GREEN}sail ps${NC} Display the status of all containers" + echo + echo "${YELLOW}Artisan Commands:${NC}" + echo " ${GREEN}sail artisan ...${NC} Run an Artisan command" + echo " ${GREEN}sail artisan queue:work${NC}" + echo + echo "${YELLOW}PHP Commands:${NC}" + echo " ${GREEN}sail php ...${NC} Run a snippet of PHP code" + echo " ${GREEN}sail php -v${NC}" + echo + echo "${YELLOW}Composer Commands:${NC}" + echo " ${GREEN}sail composer ...${NC} Run a Composer command" + echo " ${GREEN}sail composer require laravel/sanctum${NC}" + echo + echo "${YELLOW}Node Commands:${NC}" + echo " ${GREEN}sail node ...${NC} Run a Node command" + echo " ${GREEN}sail node --version${NC}" + echo + echo "${YELLOW}NPM Commands:${NC}" + echo " ${GREEN}sail npm ...${NC} Run a npm command" + echo " ${GREEN}sail npx${NC} Run a npx command" + echo " ${GREEN}sail npm run prod${NC}" + echo + echo "${YELLOW}PNPM Commands:${NC}" + echo " ${GREEN}sail pnpm ...${NC} Run a pnpm command" + echo " ${GREEN}sail pnpx${NC} Run a pnpx command" + echo " ${GREEN}sail pnpm run prod${NC}" + echo + echo "${YELLOW}Yarn Commands:${NC}" + echo " ${GREEN}sail yarn ...${NC} Run a Yarn command" + echo " ${GREEN}sail yarn run prod${NC}" + echo + echo "${YELLOW}Bun Commands:${NC}" + echo " ${GREEN}sail bun ...${NC} Run a bun command" + echo " ${GREEN}sail bunx${NC} Run a bunx command" + echo " ${GREEN}sail bun run prod${NC}" + echo + echo "${YELLOW}Database Commands:${NC}" + echo " ${GREEN}sail mysql${NC} Start a MySQL CLI session within the 'mysql' container" + echo " ${GREEN}sail mariadb${NC} Start a MySQL CLI session within the 'mariadb' container" + echo " ${GREEN}sail psql${NC} Start a PostgreSQL CLI session within the 'pgsql' container" + echo " ${GREEN}sail mongodb${NC} Start a Mongo Shell session within the 'mongodb' container" + echo " ${GREEN}sail redis${NC} Start a Redis CLI session within the 'redis' container" + echo + echo "${YELLOW}Debugging:${NC}" + echo " ${GREEN}sail debug ...${NC} Run an Artisan command in debug mode" + echo " ${GREEN}sail debug queue:work${NC}" + echo + echo "${YELLOW}Running Tests:${NC}" + echo " ${GREEN}sail test${NC} Run the PHPUnit tests via the Artisan test command" + echo " ${GREEN}sail phpunit ...${NC} Run PHPUnit" + echo " ${GREEN}sail pest ...${NC} Run Pest" + echo " ${GREEN}sail pint ...${NC} Run Pint" + echo " ${GREEN}sail dusk${NC} Run the Dusk tests (Requires the laravel/dusk package)" + echo " ${GREEN}sail dusk:fails${NC} Re-run previously failed Dusk tests (Requires the laravel/dusk package)" + echo + echo "${YELLOW}Container CLI:${NC}" + echo " ${GREEN}sail shell${NC} Start a shell session within the application container" + echo " ${GREEN}sail bash${NC} Alias for 'sail shell'" + echo " ${GREEN}sail root-shell${NC} Start a root shell session within the application container" + echo " ${GREEN}sail root-bash${NC} Alias for 'sail root-shell'" + echo " ${GREEN}sail tinker${NC} Start a new Laravel Tinker session" + echo + echo "${YELLOW}Sharing:${NC}" + echo " ${GREEN}sail share${NC} Share the application publicly via a temporary URL" + echo " ${GREEN}sail open${NC} Open the site in your browser" + echo + echo "${YELLOW}Binaries:${NC}" + echo " ${GREEN}sail bin ...${NC} Run Composer binary scripts from the vendor/bin directory" + echo + echo "${YELLOW}Customization:${NC}" + echo " ${GREEN}sail artisan sail:publish${NC} Publish the Sail configuration files" + echo " ${GREEN}sail build --no-cache${NC} Rebuild all of the Sail containers" + + exit 1 +} + +# Proxy the "help" command... +if [ $# -gt 0 ]; then + if [ "$1" == "help" ] || [ "$1" == "-h" ] || [ "$1" == "-help" ] || [ "$1" == "--help" ]; then + display_help + fi +else + display_help +fi + +# Source the ".env" file so Laravel's environment variables are available... +# shellcheck source=/dev/null +if [ -n "$APP_ENV" ] && [ -f ./.env."$APP_ENV" ]; then + source ./.env."$APP_ENV"; +elif [ -f ./.env ]; then + source ./.env; +fi + +# Define environment variables... +export APP_PORT=${APP_PORT:-80} +export APP_SERVICE=${APP_SERVICE:-"laravel.test"} +export DB_PORT=${DB_PORT:-3306} +export WWWUSER=${WWWUSER:-$UID} +export WWWGROUP=${WWWGROUP:-$(id -g)} + +export SAIL_FILES=${SAIL_FILES:-""} +export SAIL_SHARE_DASHBOARD=${SAIL_SHARE_DASHBOARD:-4040} +export SAIL_SHARE_SERVER_HOST=${SAIL_SHARE_SERVER_HOST:-"laravel-sail.site"} +export SAIL_SHARE_SERVER_PORT=${SAIL_SHARE_SERVER_PORT:-8080} +export SAIL_SHARE_SUBDOMAIN=${SAIL_SHARE_SUBDOMAIN:-""} +export SAIL_SHARE_DOMAIN=${SAIL_SHARE_DOMAIN:-"$SAIL_SHARE_SERVER_HOST"} +export SAIL_SHARE_SERVER=${SAIL_SHARE_SERVER:-""} + +# Function that outputs Sail is not running... +function sail_is_not_running { + echo "${BOLD}Sail is not running.${NC}" >&2 + echo "" >&2 + echo "${BOLD}You may Sail using the following commands:${NC} './vendor/bin/sail up' or './vendor/bin/sail up -d'" >&2 + + exit 1 +} + +# Define Docker Compose command prefix... +if docker compose &> /dev/null; then + DOCKER_COMPOSE=(docker compose) +else + DOCKER_COMPOSE=(docker-compose) +fi + +if [ -n "$SAIL_FILES" ]; then + # Convert SAIL_FILES to an array... + IFS=':' read -ra SAIL_FILES <<< "$SAIL_FILES" + + for FILE in "${SAIL_FILES[@]}"; do + if [ -f "$FILE" ]; then + DOCKER_COMPOSE+=(-f "$FILE") + else + echo "${BOLD}Unable to find Docker Compose file: '${FILE}'${NC}" >&2 + + exit 1 + fi + done +fi + +EXEC="yes" + +if [ -z "$SAIL_SKIP_CHECKS" ]; then + # Ensure that Docker is running... + if ! docker info > /dev/null 2>&1; then + echo "${BOLD}Docker is not running.${NC}" >&2 + + exit 1 + fi + + # Determine if Sail is currently up... + if "${DOCKER_COMPOSE[@]}" ps "$APP_SERVICE" 2>&1 | grep 'Exit\|exited'; then + echo "${BOLD}Shutting down old Sail processes...${NC}" >&2 + + "${DOCKER_COMPOSE[@]}" down > /dev/null 2>&1 + + EXEC="no" + elif [ -z "$("${DOCKER_COMPOSE[@]}" ps -q)" ]; then + EXEC="no" + fi +fi + +ARGS=() + +# Proxy PHP commands to the "php" binary on the application container... +if [ "$1" == "php" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" "php") + else + sail_is_not_running + fi + +# Proxy vendor binary commands on the application container... +elif [ "$1" == "bin" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + CMD=$1 + shift 1 + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" ./vendor/bin/"$CMD") + else + sail_is_not_running + fi + +# Proxy docker-compose commands to the docker-compose binary on the application container... +elif [ "$1" == "docker-compose" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" "${DOCKER_COMPOSE[@]}") + else + sail_is_not_running + fi + +# Proxy Composer commands to the "composer" binary on the application container... +elif [ "$1" == "composer" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" "composer") + else + sail_is_not_running + fi + +# Proxy Artisan commands to the "artisan" binary on the application container... +elif [ "$1" == "artisan" ] || [ "$1" == "art" ] || [ "$1" == "a" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" php artisan) + else + sail_is_not_running + fi + +# Proxy the "debug" command to the "php artisan" binary on the application container with xdebug enabled... +elif [ "$1" == "debug" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u sail -e XDEBUG_TRIGGER=1) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" php artisan) + else + sail_is_not_running + fi + +# Proxy the "test" command to the "php artisan test" Artisan command... +elif [ "$1" == "test" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" php artisan test) + else + sail_is_not_running + fi + +# Proxy the "phpunit" command to "php vendor/bin/phpunit"... +elif [ "$1" == "phpunit" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" php vendor/bin/phpunit) + else + sail_is_not_running + fi + +# Proxy the "pest" command to "php vendor/bin/pest"... +elif [ "$1" == "pest" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" php vendor/bin/pest) + else + sail_is_not_running + fi + +# Proxy the "pint" command to "php vendor/bin/pint"... +elif [ "$1" == "pint" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" php vendor/bin/pint) + else + sail_is_not_running + fi + +# Proxy the "dusk" command to the "php artisan dusk" Artisan command... +elif [ "$1" == "dusk" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=(-e "APP_URL=http://${APP_SERVICE}") + ARGS+=(-e "DUSK_DRIVER_URL=http://selenium:4444/wd/hub") + ARGS+=("$APP_SERVICE" php artisan dusk) + else + sail_is_not_running + fi + +# Proxy the "dusk:fails" command to the "php artisan dusk:fails" Artisan command... +elif [ "$1" == "dusk:fails" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=(-e "APP_URL=http://${APP_SERVICE}") + ARGS+=(-e "DUSK_DRIVER_URL=http://selenium:4444/wd/hub") + ARGS+=("$APP_SERVICE" php artisan dusk:fails) + else + sail_is_not_running + fi + +# Initiate a Laravel Tinker session within the application container... +elif [ "$1" == "tinker" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" php artisan tinker) + else + sail_is_not_running + fi + +# Proxy Node commands to the "node" binary on the application container... +elif [ "$1" == "node" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" node) + else + sail_is_not_running + fi + +# Proxy NPM commands to the "npm" binary on the application container... +elif [ "$1" == "npm" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" npm) + else + sail_is_not_running + fi + +# Proxy NPX commands to the "npx" binary on the application container... +elif [ "$1" == "npx" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" npx) + else + sail_is_not_running + fi + +# Proxy PNPM commands to the "pnpm" binary on the application container... +elif [ "$1" == "pnpm" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" pnpm) + else + sail_is_not_running + fi + +# Proxy PNPX commands to the "pnpx" binary on the application container... +elif [ "$1" == "pnpx" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" pnpx) + else + sail_is_not_running + fi + +# Proxy Yarn commands to the "yarn" binary on the application container... +elif [ "$1" == "yarn" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" yarn) + else + sail_is_not_running + fi + +# Proxy Bun commands to the "bun" binary on the application container... +elif [ "$1" == "bun" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" bun) + else + sail_is_not_running + fi + +# Proxy Bun X commands to the "bunx" binary on the application container... +elif [ "$1" == "bunx" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" bunx) + else + sail_is_not_running + fi + +# Initiate a MySQL CLI terminal session within the "mysql" container... +elif [ "$1" == "mysql" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=(mysql bash -c) + ARGS+=("MYSQL_PWD=\${MYSQL_PASSWORD} mysql -u \${MYSQL_USER} \${MYSQL_DATABASE}") + else + sail_is_not_running + fi + +# Initiate a MySQL CLI terminal session within the "mariadb" container... +elif [ "$1" == "mariadb" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=(mariadb bash -c) + ARGS+=("MYSQL_PWD=\${MYSQL_PASSWORD} mariadb -u \${MYSQL_USER} \${MYSQL_DATABASE}") + else + sail_is_not_running + fi + +# Initiate a PostgreSQL CLI terminal session within the "pgsql" container... +elif [ "$1" == "psql" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=(pgsql bash -c) + ARGS+=("PGPASSWORD=\${PGPASSWORD} psql -U \${POSTGRES_USER} \${POSTGRES_DB}") + else + sail_is_not_running + fi + +# Initiate a Bash shell within the application container... +elif [ "$1" == "shell" ] || [ "$1" == "bash" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" bash) + else + sail_is_not_running + fi + +# Initiate a root user Bash shell within the application container... +elif [ "$1" == "root-shell" ] || [ "$1" == "root-bash" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec -u root) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" bash) + else + sail_is_not_running + fi + +# Initiate a MongoDB Shell within the "mongodb" container... +elif [ "$1" == "mongodb" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=(mongodb mongosh --port "${FORWARD_MONGODB_PORT:-27017}" --username "$MONGODB_USERNAME" --password "$MONGODB_PASSWORD" --authenticationDatabase admin) + else + sail_is_not_running + fi + +# Initiate a Redis CLI terminal session within the "redis" container... +elif [ "$1" == "redis" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + ARGS+=(exec) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=(redis redis-cli) + else + sail_is_not_running + fi + +# Share the site... +elif [ "$1" == "share" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + docker run --init --rm --add-host=host.docker.internal:host-gateway -p "$SAIL_SHARE_DASHBOARD":4040 -t beyondcodegmbh/expose-server:latest share http://host.docker.internal:"$APP_PORT" \ + --server-host="$SAIL_SHARE_SERVER_HOST" \ + --server-port="$SAIL_SHARE_SERVER_PORT" \ + --auth="$SAIL_SHARE_TOKEN" \ + --server="$SAIL_SHARE_SERVER" \ + --subdomain="$SAIL_SHARE_SUBDOMAIN" \ + --domain="$SAIL_SHARE_DOMAIN" \ + "$@" + + exit + else + sail_is_not_running + fi + +# Open the site... +elif [ "$1" == "open" ]; then + shift 1 + + if command -v open &>/dev/null; then + OPEN="open" + elif command -v xdg-open &>/dev/null; then + OPEN="xdg-open" + else + echo "Neither open nor xdg-open is available. Exiting." + exit 1 + fi + + if [ "$EXEC" == "yes" ]; then + + if [[ -n "$APP_PORT" && "$APP_PORT" != "80" ]]; then + FULL_URL="${APP_URL}:${APP_PORT}" + else + FULL_URL="$APP_URL" + fi + + $OPEN "$FULL_URL" + + exit + else + sail_is_not_running + fi +fi + +# Run Docker Compose with the defined arguments... +"${DOCKER_COMPOSE[@]}" "${ARGS[@]}" "$@" diff --git a/src/Classes/AbstractMySqlSchemaStateProxy.php b/src/Classes/AbstractMySqlSchemaStateProxy.php index a74f53c..064c62b 100644 --- a/src/Classes/AbstractMySqlSchemaStateProxy.php +++ b/src/Classes/AbstractMySqlSchemaStateProxy.php @@ -58,4 +58,9 @@ protected function appendMigrationData(string $path): void { $this->schemaState->appendMigrationData(...func_get_args()); } + + public function getConditionalParameters(): array + { + return []; + } } diff --git a/src/Classes/AbstractPostgresSchemaStateProxy.php b/src/Classes/AbstractPostgresSchemaStateProxy.php index fdae46e..0bd4d1b 100644 --- a/src/Classes/AbstractPostgresSchemaStateProxy.php +++ b/src/Classes/AbstractPostgresSchemaStateProxy.php @@ -59,4 +59,9 @@ protected function getBaseDumpArguments(): array '--dbname="${:LARAVEL_LOAD_DATABASE}"', ]; } + + public function getConditionalParameters(): array + { + return []; + } } diff --git a/src/Classes/MySqlSchemaStateProxy.php b/src/Classes/MySqlSchemaStateProxy.php index 8573106..a336aa9 100644 --- a/src/Classes/MySqlSchemaStateProxy.php +++ b/src/Classes/MySqlSchemaStateProxy.php @@ -32,14 +32,6 @@ public function dump(Connection $connection, $path) } } - /** - * @inheritDoc - */ - public function load($path) - { - $this->schemaState->load(...func_get_args()); - } - /** * Get the dump command for MySQL as a string. */ @@ -47,14 +39,6 @@ protected function getCommandString(): string { $command = 'mysqldump '.$this->schemaState->connectionString().' '; - $conditionalParameters = [ - '--set-gtid-purged=OFF' => !$this->schemaState->connection->isMaria(), - '--no-create-db' => !$this->protector->shouldCreateDb(), - '--skip-comments' => !$this->protector->shouldDumpComments(), - '--skip-set-charset' => !$this->protector->shouldDumpCharsets(), - '--no-data' => !$this->protector->shouldDumpData(), - ]; - $parameters = [ '--add-locks', '--routines', @@ -62,10 +46,22 @@ protected function getCommandString(): string '--column-statistics=0', '--result-file="${:LARAVEL_LOAD_PATH}"', '--max-allowed-packet='.$this->protector->getMaxPacketLength(), - ...array_keys(array_filter($conditionalParameters)), + ...array_keys(array_filter($this->getConditionalParameters())), '"${:LARAVEL_LOAD_DATABASE}"', ]; return $command.implode(' ', $parameters); } + + public function getConditionalParameters(): array + { + return [ + '--set-gtid-purged=OFF' => !$this->schemaState->connection->isMaria(), + '--no-create-db' => !$this->protector->shouldCreateDb(), + '--skip-comments' => !$this->protector->shouldDumpComments(), + '--skip-set-charset' => !$this->protector->shouldDumpCharsets(), + '--no-data' => !$this->protector->shouldDumpData(), + '--no-tablespaces' => !$this->protector->shouldUseTablespaces(), + ]; + } } diff --git a/src/Classes/PostgresSchemaStateProxy.php b/src/Classes/PostgresSchemaStateProxy.php index c5feb10..24e746f 100644 --- a/src/Classes/PostgresSchemaStateProxy.php +++ b/src/Classes/PostgresSchemaStateProxy.php @@ -25,26 +25,22 @@ public function dump(Connection $connection, $path) ])); } - /** - * {@inheritDoc} - */ - public function load($path) + protected function getBaseDumpArguments(): array { - $this->schemaState->load(...func_get_args()); + return [ + ...parent::getBaseDumpArguments(), + ...array_keys(array_filter($this->getConditionalParameters())), + ]; } - - protected function getBaseDumpArguments(): array + + public function getConditionalParameters(): array { - $conditionalArguments = [ + return [ '--create' => $this->protector->shouldCreateDb(), '--clean' => $this->protector->shouldCreateDb() && $this->protector->shouldDropDb(), '--verbose' => $this->protector->shouldDumpComments(), '--schema-only' => !$this->protector->shouldDumpData(), - ]; - - return [ - ...parent::getBaseDumpArguments(), - ...array_keys(array_filter($conditionalArguments)), + '--no-tablespaces' => !$this->protector->shouldUseTablespaces(), ]; } } diff --git a/src/ProtectorServiceProvider.php b/src/ProtectorServiceProvider.php index cfa6896..db971cd 100644 --- a/src/ProtectorServiceProvider.php +++ b/src/ProtectorServiceProvider.php @@ -3,6 +3,7 @@ namespace Cybex\Protector; use Cybex\Protector\Classes\MySqlSchemaStateProxy; +use Cybex\Protector\Classes\PostgresSchemaStateProxy; use Cybex\Protector\Commands\CreateKeys; use Cybex\Protector\Commands\CreateToken; use Cybex\Protector\Commands\ExportDump; @@ -54,6 +55,10 @@ public function register(): void $this->app->bind(MySqlSchemaStateProxy::class, function ($app, array $params) { return new MySqlSchemaStateProxy(...$params); }); + + $this->app->bind(PostgresSchemaStateProxy::class, function ($app, array $params) { + return new PostgresSchemaStateProxy(...$params); + }); } protected function registerRoutes() diff --git a/src/Traits/HasConfiguration.php b/src/Traits/HasConfiguration.php index 5a37c2e..1403f5f 100644 --- a/src/Traits/HasConfiguration.php +++ b/src/Traits/HasConfiguration.php @@ -23,6 +23,7 @@ trait HasConfiguration /** * Defines whether existing databases should be dropped before importing a dump. + * Only works if used together with the $createDb option. * (PostgreSQL only, controls the --clean flag) */ protected bool $dropDb = true; @@ -97,6 +98,13 @@ public function withAuthTokenKeyName(string $authTokenKeyName): static return $this; } + public function withAutoIncrementingState(): static + { + $this->removeAutoIncrementingState = false; + + return $this; + } + public function withoutAutoIncrementingState(): static { $this->removeAutoIncrementingState = true; @@ -104,6 +112,13 @@ public function withoutAutoIncrementingState(): static return $this; } + public function withCharsets(): static + { + $this->dumpCharsets = true; + + return $this; + } + public function withoutCharsets(): static { $this->dumpCharsets = false; @@ -111,6 +126,13 @@ public function withoutCharsets(): static return $this; } + public function withComments(): static + { + $this->dumpComments = true; + + return $this; + } + public function withoutComments(): static { $this->dumpComments = false; @@ -118,6 +140,13 @@ public function withoutComments(): static return $this; } + public function withData(): static + { + $this->dumpData = true; + + return $this; + } + public function withoutData(): static { $this->dumpData = false; @@ -139,6 +168,13 @@ public function withConnectionName(?string $connectionName = null): static return $this; } + public function withCreateDb(): static + { + $this->createDb = true; + + return $this; + } + public function withoutCreateDb(): static { $this->createDb = false; @@ -146,6 +182,37 @@ public function withoutCreateDb(): static return $this; } + /** + * Defines that existing databases should be dropped before importing a dump. + * Only works if used together with the $createDb option. + * (PostgreSQL only, controls the --clean flag) + */ + public function withDropDb(): static + { + $this->dropDb = true; + + return $this; + } + + /** + * Defines that existing databases should not be dropped before importing a dump. + * Only works if used together with the $createDb option + * (PostgreSQL only, controls the --clean flag) + */ + public function withoutDropDb(): static + { + $this->dropDb = false; + + return $this; + } + + public function withTablespaces(): static + { + $this->tablespaces = true; + + return $this; + } + public function withoutTablespaces(): static { $this->tablespaces = false; @@ -240,6 +307,11 @@ protected function getPrivateKey(): string return env($this->privateKeyName, ''); } + public function getConnectionName(): string + { + return $this->connectionName; + } + /** * Retrieves the server url of the dump endpoint. */ @@ -277,4 +349,9 @@ public function shouldRemoveAutoIncrementingState(): bool { return $this->removeAutoIncrementingState; } + + public function shouldUseTablespaces(): bool + { + return $this->tablespaces; + } } diff --git a/tests/feature/ExportDumpTest.php b/tests/feature/ExportDumpTest.php index 53c9608..0ab6d4d 100644 --- a/tests/feature/ExportDumpTest.php +++ b/tests/feature/ExportDumpTest.php @@ -7,6 +7,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\DB; use PDOException; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -18,6 +19,40 @@ class ExportDumpTest extends TestCase protected string $filePath; protected string $emptyDumpPath; + const POSTGRES_CREATE = '--create'; + const POSTGRES_CLEAN = '--clean'; + const POSTGRES_VERBOSE = '--verbose'; + const POSTGRES_SCHEMA_ONLY = '--schema-only'; + const NO_TABLESPACES = '--no-tablespaces'; + const MYSQL_SKIP_GTID_INFO = '--set-gtid-purged=OFF'; + const MYSQL_NO_CREATE_DB = '--no-create-db'; + const MYSQL_SKIP_COMMENTS = '--skip-comments'; + const MYSQL_SKIP_SET_CHARSET = '--skip-set-charset'; + const MYSQL_NO_DATA = '--no-data'; + const PROTECTOR_WITH_CREATE_DB = 'withCreateDb'; + const PROTECTOR_WITH_DROP_DB = 'withDropDb'; + const PROTECTOR_WITH_COMMENTS = 'withComments'; + const PROTECTOR_WITH_CHARSETS = 'withCharsets'; + const PROTECTOR_WITH_DATA = 'withData'; + const PROTECTOR_WITH_TABLESPACES = 'withTablespaces'; + const PROTECTOR_CONFIG_BASELINE = [ + 'pgsql' => [ + self::POSTGRES_CREATE => false, + self::POSTGRES_CLEAN => false, + self::POSTGRES_VERBOSE => false, + self::POSTGRES_SCHEMA_ONLY => true, + self::NO_TABLESPACES => true, + ], + 'mysql' => [ + self::MYSQL_SKIP_GTID_INFO => true, + self::MYSQL_NO_CREATE_DB => true, + self::MYSQL_SKIP_COMMENTS => true, + self::MYSQL_SKIP_SET_CHARSET => true, + self::MYSQL_NO_DATA => true, + self::NO_TABLESPACES => true, + ], + ]; + protected function setUp(): void { parent::setUp(); @@ -25,7 +60,7 @@ protected function setUp(): void $this->disk = $this->getFakeDumpDisk(); $this->baseDirectory = Config::get('protector.baseDirectory'); - $this->filePath = sprintf('%s/dump.sql', $this->baseDirectory); + $this->filePath = sprintf('%s/dump.sql', $this->baseDirectory); $this->emptyDumpPath = 'testDumps/dump.sql'; } @@ -36,7 +71,7 @@ public function createDestinationFilePath() { $this->disk->deleteDirectory(Config::get('protector.baseDirectory')); - $filePath = $this->protector->createDestinationFilePath(__FUNCTION__); + $filePath = $this->protector->createDestinationFilePath(__FUNCTION__); $destinationFilePath = $this->disk->path($filePath); $this->runProtectedMethod('createDirectory', [$filePath, $this->disk]); @@ -50,7 +85,7 @@ public function createDestinationFilePathWithSubFolder() { $this->disk->deleteDirectory(Config::get('protector.baseDirectory')); - $filePath = $this->protector->createDestinationFilePath(__FUNCTION__, __FUNCTION__); + $filePath = $this->protector->createDestinationFilePath(__FUNCTION__, __FUNCTION__); $destinationFilePath = $this->disk->path($filePath); $this->runProtectedMethod('createDirectory', [$filePath, $this->disk]); @@ -99,10 +134,10 @@ public function failGeneratingDumpWhenTryingToConnectToDatabase() { // Provide an database connection to a non-existing database. Config::set('database.connections.invalid', [ - 'driver' => 'mysql', - 'url' => env('DATABASE_URL'), - 'host' => env('DB_HOST', '127.0.0.1'), - 'port' => env('DB_PORT', '3306'), + 'driver' => 'mysql', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), 'database' => 'invalid_database_name', 'username' => env('DB_USERNAME', 'forge'), 'password' => env('DB_PASSWORD', ''), @@ -128,4 +163,173 @@ public function createsStreamedFileDownloadResponse() $this->assertInstanceOf(StreamedResponse::class, $response); $this->assertEquals(200, $response->getStatusCode()); } + + /** + * @test + * @dataProvider provideForHasCorrectConfiguration + */ + public function hasCorrectConfiguration(array $protectorOptions, array $expected): void + { + $this->configureProtector($protectorOptions); + + $connection = DB::connection($this->protector->getConnectionName()); + $schemaState = $connection->getSchemaState(); + $schemaStateProxy = $this->runProtectedMethod('getProxyForSchemaState', [$schemaState]); + + $conditionalParameters = $schemaStateProxy->getConditionalParameters(); + + $this->assertEquals($expected[$connection->getDriverName()], $conditionalParameters); + } + + public static function provideForHasCorrectConfiguration(): array + { + return [ + 'all options on' => [ + 'protectorOptions' => [ + self::PROTECTOR_WITH_CREATE_DB, + self::PROTECTOR_WITH_DROP_DB, + self::PROTECTOR_WITH_COMMENTS, + self::PROTECTOR_WITH_CHARSETS, + self::PROTECTOR_WITH_DATA, + self::PROTECTOR_WITH_TABLESPACES, + ], + 'expected' => self::getExpected([ + 'pgsql' => [ + self::POSTGRES_CREATE => true, + self::POSTGRES_CLEAN => true, + self::POSTGRES_VERBOSE => true, + self::POSTGRES_SCHEMA_ONLY => false, + self::NO_TABLESPACES => false, + ], + 'mysql' => [ + self::MYSQL_SKIP_GTID_INFO => true, + self::MYSQL_NO_CREATE_DB => false, + self::MYSQL_SKIP_COMMENTS => false, + self::MYSQL_SKIP_SET_CHARSET => false, + self::MYSQL_NO_DATA => false, + self::NO_TABLESPACES => false, + ] + ]) + ], + 'all options off' => [ + 'protectorOptions' => [], + 'expected' => self::getExpected() + ], + 'postgres purge db' => [ + 'protectorOptions' => [ + self::PROTECTOR_WITH_CREATE_DB, + self::PROTECTOR_WITH_DROP_DB + ], + 'expected' => self::getExpected([ + 'pgsql' => [ + self::POSTGRES_CREATE => true, + self::POSTGRES_CLEAN => true, + ], + 'mysql' => [ + self::MYSQL_NO_CREATE_DB => false, + ], + ]) + ], + 'create db' => [ + 'protectorOptions' => [ + self::PROTECTOR_WITH_CREATE_DB + ], + 'expected' => self::getExpected([ + 'pgsql' => [ + self::POSTGRES_CREATE => true, + ], + 'mysql' => [ + self::MYSQL_NO_CREATE_DB => false, + ], + ]) + ], + 'drop db' => [ + 'protectorOptions' => [ + self::PROTECTOR_WITH_DROP_DB + ], + 'expected' => self::getExpected() + ], + 'dump comments' => [ + 'protectorOptions' => [ + self::PROTECTOR_WITH_COMMENTS + ], + 'expected' => self::getExpected([ + 'pgsql' => [ + self::POSTGRES_VERBOSE => true, + ], + 'mysql' => [ + self::MYSQL_SKIP_COMMENTS => false, + ], + ]) + ], + 'dump charsets' => [ + 'protectorOptions' => [ + self::PROTECTOR_WITH_CHARSETS + ], + 'expected' => self::getExpected([ + 'mysql' => [ + self::MYSQL_SKIP_SET_CHARSET => false, + ], + ]) + ], + 'dump data' => [ + 'protectorOptions' => [ + self::PROTECTOR_WITH_DATA + ], + 'expected' => self::getExpected([ + 'pgsql' => [ + self::POSTGRES_SCHEMA_ONLY => false, + ], + 'mysql' => [ + self::MYSQL_NO_DATA => false, + ], + ]) + ], + 'dump tablespaces' => [ + 'protectorOptions' => [ + self::PROTECTOR_WITH_TABLESPACES + ], + 'expected' => self::getExpected([ + 'pgsql' => [ + self::NO_TABLESPACES => false, + ], + 'mysql' => [ + self::NO_TABLESPACES => false, + ], + ]) + ], + ]; + } + + /** + * Merges the baseline array (all protector config options set to 'without') with the provided deviations. + * + * @param array $deviations + * @return array + */ + protected static function getExpected(array $deviations = []): array + { + return array_replace_recursive(self::PROTECTOR_CONFIG_BASELINE, $deviations); + } + + /** + * Configures the protector with the provided options. + * + * @param array $protectorOptions + * @return void + */ + protected function configureProtector(array $protectorOptions): void + { + $this->protector + ->withoutCreateDb() + ->withoutDropDb() + ->withoutComments() + ->withoutCharsets() + ->withoutData() + ->withoutTablespaces(); + + foreach ($protectorOptions as $option) { + $this->protector->$option(); + } + } }