From e18efafe97596d9d767d1f6fc41831f66ee54cf2 Mon Sep 17 00:00:00 2001 From: Gael Connan Date: Fri, 15 Jul 2022 11:55:43 +0200 Subject: [PATCH 1/2] add tests. --- .github/workflows/main.yml | 45 ++++++ .gitignore | 1 - README.md | 12 +- phpunit.xml | 31 ++++ tests/AbstractTestCase.php | 23 +++ tests/MeltorTests.php | 320 +++++++++++++++++++++++++++++++++++++ 6 files changed, 428 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/main.yml create mode 100644 phpunit.xml create mode 100644 tests/AbstractTestCase.php create mode 100644 tests/MeltorTests.php diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..5f12a1b --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,45 @@ +name: run-tests + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: [ubuntu-latest, windows-latest] + php: [7.4, 8.0] + laravel: [8.*] + stability: [prefer-lowest, prefer-stable] + include: + - laravel: 8.* + testbench: ^6.6 + + name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo + coverage: none + + - name: Setup problem matchers + run: | + echo "::add-matcher::${{ runner.tool_cache }}/php.json" + echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + - name: Install dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update + composer update --${{ matrix.stability }} --prefer-dist --no-interaction + - name: Execute tests + run: vendor/bin/phpunit diff --git a/.gitignore b/.gitignore index 6e371a7..ea43bd1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ /.idea/* -/coverage .env npm-debug.log yarn-error.log diff --git a/README.md b/README.md index a879984..36c2f72 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Configuring the root user to access the database with sufficient permissions is To create a new migration file: -```php +```bash php artisan meltor:generate ``` @@ -84,7 +84,7 @@ Notes: To also do a comparison between the old and the new database: -```php +```bash php artisan meltor:generate --testrun ``` @@ -99,7 +99,7 @@ This package uses the laravel-protector package to back up your database during The backup file is in the default protector folder, by default `storage/app/protector/meltorTestrunBackup.sql`. In case the `artisan meltor:generate --testrun` command has crashed, you can restore the previous DB state: -```php +```bash php artisan meltor:generate --restore ``` @@ -135,6 +135,12 @@ Exceptions: - [Cybex Web Development Team](https://github.com/cybex-gmbh) - [All Contributors](../../contributors) +## Testing + +```bash +vendor/bin/phpunit tests/MeltorTests.php +``` + ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..85acf0c --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,31 @@ + + + + + ./tests + + + + + ./src + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/AbstractTestCase.php b/tests/AbstractTestCase.php new file mode 100644 index 0000000..b832bba --- /dev/null +++ b/tests/AbstractTestCase.php @@ -0,0 +1,23 @@ +setupDb(); + } + + /** + * @test + * @return void + */ + public function canReadConfig(): void + { + Config::set('meltor.string', 'stringResult'); + Config::set('meltor.subkey.string', 'stringResult'); + + $this->assertEquals(app('meltor')->config('string'), 'stringResult'); + $this->assertEquals(app('meltor')->config('subkey.string'), 'stringResult'); + + Config::set('meltor.closure', fn() => 'closureResult'); + Config::set('meltor.subkey.closure', fn() => 'closureResult'); + + $this->assertEquals(app('meltor')->config('closure'), 'closureResult'); + $this->assertEquals(app('meltor')->config('subkey.closure'), 'closureResult'); + } + + /** + * @test + * @return void + */ + public function canStripDatabaseName(): void + { + $this->assertEquals(app('meltor')->stripDatabaseName('database_name/table_name'), 'table_name'); + $this->assertEquals(app('meltor')->stripDatabaseName('table_name'), 'table_name'); + } + + /** + * @test + * @return void + */ + public function canGetDatabaseConfigOfProject(): void + { + $this->assertTrue(array_key_exists('driver', app('meltor')->getDatabaseConfig())); + } + + + /** + * @test + * @return void + */ + public function canGetDatabaseStructure(): void + { + $result = app('meltor')->getDatabaseStructure('meltor', app('db')->connection('information_schema')); + + $this->assertArrayHasKey('migrations', $result); + } + + /** + * @test + * @return void + */ + public function canGetUniqueKeys(): void + { + $result = app('meltor')->getUniqueKeys('meltor', app('db')->connection('information_schema')); + + $this->assertArrayHasKey('meltor_all_types_test', $result); + $this->assertEquals('unique_one_field', $result['meltor_all_types_test'][0]->INDEX_NAME); + } + + /** + * @test + * @return void + */ + public function canGetForeignKeys(): void + { + $result = app('meltor')->getForeignKeys('meltor', app('db')->connection('information_schema')); + + $this->assertArrayHasKey('meltor_all_types_test', $result); + $this->assertEquals('meltor_foreign_id', $result['meltor_all_types_test'][0]->COLUMN_NAME); + } + + /** + * @test + * @return void + */ + public function canGetDataTypeFromColumn(): void + { + $validColumn = $this->getDummyColumn(); + $validBoolColumn = $this->getDummyColumn('int', 'tinyint(1)'); + $invalidFileTypeColumn = $this->getDummyColumn('int', 'int', 'unknown type'); + + // Special boolean handling + $this->assertEquals('boolean', $this->runProtectedMethod('getDataType', [$validBoolColumn])); + + // Regular handling + $this->assertEquals('int', $this->runProtectedMethod('getDataType', [$validColumn])); + + // Invalid MySQL DATA_TYPE, ignore problems mode + $this->assertNull($this->runProtectedMethod('getDataType', [$invalidFileTypeColumn, true])); + + // Invalid MySQL DATA_TYPE + $this->expectException(Exception::class); + $this->runProtectedMethod('getDataType', [$invalidFileTypeColumn]); + } + + /** + * @test + * @return void + */ + public function canGetExtraFromColumn(): void + { + $validColumn = $this->getDummyColumn(); + $validAutoIncrementColumn = $this->getDummyColumn('int', 'int', 'int', 'auto_increment'); + + $this->assertEquals('auto_increment', $this->runProtectedMethod('getExtra', [$validAutoIncrementColumn])); + $this->assertEquals('', $this->runProtectedMethod('getExtra', [$validColumn])); + } + + /** + * @test + * @return void + */ + public function canGetColumnType(): void + { + $validColumn = $this->getDummyColumn(); + $invalidTypeColumn = $this->getDummyColumn('int', 'foo'); + + $this->assertEquals('int', $this->runProtectedMethod('getColumnType', [$validColumn])); + + // Invalid MySQL COLUMN_TYPE, problems ignored + $this->assertNull($this->runProtectedMethod('getColumnType', [$invalidTypeColumn, true])); + + // Invalid MySQL COLUMN_TYPE + $this->expectException(Exception::class); + $this->runProtectedMethod('getColumnType', [$invalidTypeColumn]); + } + + /** + * @test + * @return void + */ + public function canGetDisplayWidth(): void + { + $validColumn = $this->getDummyColumn(); + $invalidTypeColumn = $this->getDummyColumn('int', 'foo'); + + $this->assertEquals('int', $this->runProtectedMethod('getColumnType', [$validColumn])); + + // Invalid MySQL COLUMN_TYPE, problems ignored + $this->assertNull($this->runProtectedMethod('getColumnType', [$invalidTypeColumn, true])); + + // Invalid MySQL COLUMN_TYPE + $this->expectException(Exception::class); + $this->runProtectedMethod('getColumnType', [$invalidTypeColumn]); + } + + + // ===================================================================== + + + protected function setupDb() + { + Config::set('database.default', 'mysql'); + + Config::set( + 'database.connections.mysql', + [ + 'driver' => 'mysql', + 'url' => null, + 'host' => 'laravel-meltor-mysql-1', + 'port' => '3306', + 'database' => 'meltor', + 'username' => 'homestead', + 'password' => 'secret', + 'unix_socket' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter( + [ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ] + ) : [], + ], + ); + + Config::set( + 'database.connections.information_schema', + [ + 'driver' => 'mysql', + 'url' => null, + 'host' => 'laravel-meltor-mysql-1', + 'port' => '3306', + 'database' => 'information_schema', + 'username' => 'root', + 'password' => 'secret', + 'unix_socket' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter( + [ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ] + ) : [], + ], + ); + + Artisan::call('migrate:fresh'); + + if (!Schema::hasTable('meltor_foreigns')) { + Schema::create('meltor_foreigns', function (Blueprint $table) { + $table->id(); + }); + } + + if (!Schema::hasTable('meltor_all_types_test')) { + Schema::create('meltor_all_types_test', function (Blueprint $table) { + $table->tinyInteger('tinyint_field'); + $table->boolean('boolean_field'); + $table->bigInteger('bigint_field'); + $table->mediumInteger('mediumint_field'); + $table->smallInteger('smallint_field'); + $table->integer('int_field'); + $table->decimal('decimal_field'); + $table->float('float_field'); + $table->double('double_field'); + $table->date('date_field'); + $table->timestamp('timestamp_field'); + $table->dateTime('datetime_field'); + $table->time('time_field'); + $table->year('year_field'); + $table->char('char_field'); + $table->string('varchar_field'); + $table->tinyText('tinytext_field'); + $table->mediumText('mediumtext_field'); + $table->text('text_field'); + $table->longText('longtext_field'); + $table->binary('blob_field'); + $table->geometry('geometry_field'); + $table->point('point_field'); + $table->lineString('linestring_field'); + $table->polygon('polygon_field'); + $table->multiPoint('multipoint_field'); + $table->multiLineString('multilinestring_field'); + $table->multiPolygon('multipolygon_field'); + $table->json('json_field'); + $table->unique('int_field', 'unique_one_field'); + $table->foreignId('meltor_foreign_id')->constrained(); + }); + } + } + + /** + * @param $method + * + * @return ReflectionMethod + */ + protected function getAccessibleReflectionMethod($method): ReflectionMethod + { + $reflectionProtector = new ReflectionClass(app('meltor')); + $method = $reflectionProtector->getMethod($method); + + $method->setAccessible(true); + + return $method; + } + + /** + * Allows a test to call a protected method. + * + * @param string $methodName + * @param array $params + * + * @return mixed + */ + protected function runProtectedMethod(string $methodName, array $params) + { + $method = $this->getAccessibleReflectionMethod($methodName); + return $method->invoke(app('meltor'), ...$params); + } + + protected function getDummyColumn(string $mysqlType = 'int', string $columnType = 'int', string $dataType = 'int', string $extra = ''): stdClass + { + $column = new \stdClass(); + $column->COLUMN_NAME = $mysqlType . '_field'; + $column->COLUMN_TYPE = $columnType; + $column->DATA_TYPE = $dataType; + $column->IS_NULLABLE = false; + $column->CHARACTER_SET_NAME = null; + $column->COLLATION_NAME = null; + $column->COLUMN_COMMENT = null; + $column->EXTRA = $extra; + $column->COLUMN_DEFAULT = null; + + return $column; + } +} \ No newline at end of file From 62d3af0f62ee490c6546562eae8ff2fafd44a9f4 Mon Sep 17 00:00:00 2001 From: Gael Connan Date: Fri, 15 Jul 2022 11:57:49 +0200 Subject: [PATCH 2/2] add a docker environment that can run the tests locally. --- .env.example | 27 +++ docker-compose.yml | 54 +++++ docker/7.4/Dockerfile | 58 +++++ docker/7.4/php.ini | 4 + docker/7.4/start-container | 17 ++ docker/7.4/supervisord.conf | 14 ++ docker/8.0/Dockerfile | 60 +++++ docker/8.0/php.ini | 4 + docker/8.0/start-container | 17 ++ docker/8.0/supervisord.conf | 14 ++ docker/8.1/Dockerfile | 59 +++++ docker/8.1/php.ini | 4 + docker/8.1/start-container | 17 ++ docker/8.1/supervisord.conf | 14 ++ docker/sail | 360 +++++++++++++++++++++++++++ sail | 468 ++++++++++++++++++++++++++++++++++++ 16 files changed, 1191 insertions(+) create mode 100644 .env.example create mode 100644 docker-compose.yml create mode 100644 docker/7.4/Dockerfile create mode 100644 docker/7.4/php.ini create mode 100644 docker/7.4/start-container create mode 100644 docker/7.4/supervisord.conf create mode 100644 docker/8.0/Dockerfile create mode 100644 docker/8.0/php.ini create mode 100644 docker/8.0/start-container create mode 100644 docker/8.0/supervisord.conf create mode 100644 docker/8.1/Dockerfile create mode 100644 docker/8.1/php.ini create mode 100644 docker/8.1/start-container create mode 100644 docker/8.1/supervisord.conf create mode 100644 docker/sail create mode 100755 sail diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0adcd5c --- /dev/null +++ b/.env.example @@ -0,0 +1,27 @@ +APP_NAME=Meltor +APP_ENV=local +APP_KEY=base64:wwBJoK0fZxiDrABQB8p2ThxvIyy2/swCAqC4ds33RBk= +APP_DEBUG=true +APP_URL=http://meltor.test + +LOG_CHANNEL=stack + +# Docker +APP_SERVICE=app +DOCKER_CONTAINER_NAME=meltor +#DOCKER_PHP_VERSION=8.1 +# Use different DB ports if you need to expose more than one DB container +#FORWARD_DB_PORT=3306 + +DB_CONNECTION=mysql +DB_HOST=laravel-meltor-mysql-1 +DB_PORT=3306 +DB_DATABASE=meltor +DB_USERNAME=homestead +DB_PASSWORD=secret + +BROADCAST_DRIVER=log +CACHE_DRIVER=file +QUEUE_CONNECTION=sync +SESSION_DRIVER=file +SESSION_LIFETIME=120 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c1f70d6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,54 @@ +# For more information: https://laravel.com/docs/sail +# Used exclusively for testing purposes +version: '3' +services: + app: + container_name: ${DOCKER_CONTAINER_NAME:-meltor} + build: + context: ./docker/${DOCKER_PHP_VERSION:-8.1} + dockerfile: Dockerfile + args: + WWWGROUP: '${WWWGROUP}' + image: sail-${DOCKER_PHP_VERSION:-8.1}/app + extra_hosts: + - 'host.docker.internal:host-gateway' + environment: + WWWUSER: '${WWWUSER}' + LARAVEL_SAIL: 1 + XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}' + XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}' + volumes: + - '.:/var/www/html' + networks: + - shared + - internal + depends_on: + - mysql + mysql: + image: 'mysql/mysql-server:8.0' + ports: + - '${FORWARD_DB_PORT}:3306' + environment: + MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}' + MYSQL_ROOT_HOST: "%" + MYSQL_DATABASE: '${DB_DATABASE}' + MYSQL_USER: '${DB_USERNAME}' + MYSQL_PASSWORD: '${DB_PASSWORD}' + MYSQL_ALLOW_EMPTY_PASSWORD: 1 + volumes: + - 'sail-mysql:/var/lib/mysql' + networks: + - shared + - internal + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-p${DB_PASSWORD}"] + retries: 3 + timeout: 5s +networks: + internal: + internal: true + shared: + external: true +volumes: + sail-mysql: + driver: local diff --git a/docker/7.4/Dockerfile b/docker/7.4/Dockerfile new file mode 100644 index 0000000..feab9d5 --- /dev/null +++ b/docker/7.4/Dockerfile @@ -0,0 +1,58 @@ +FROM ubuntu:21.10 + +LABEL maintainer="Taylor Otwell" + +ARG WWWGROUP +ARG NODE_VERSION=16 + +WORKDIR /var/www/html + +ENV DEBIAN_FRONTEND noninteractive +ENV TZ=UTC + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN apt-get update \ + && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python2 \ + && mkdir -p ~/.gnupg \ + && chmod 600 ~/.gnupg \ + && echo "disable-ipv6" >> ~/.gnupg/dirmngr.conf \ + && apt-key adv --homedir ~/.gnupg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys E5267A6C \ + && apt-key adv --homedir ~/.gnupg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C300EE8C \ + && echo "deb http://ppa.launchpad.net/ondrej/php/ubuntu impish main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \ + && apt-get update \ + && apt-get install -y php7.4-cli php7.4-dev \ + php7.4-pgsql php7.4-sqlite3 php7.4-gd \ + php7.4-curl php7.4-memcached \ + php7.4-imap php7.4-mysql php7.4-mbstring \ + php7.4-xml php7.4-zip php7.4-bcmath php7.4-soap \ + php7.4-intl php7.4-readline php7.4-pcov \ + php7.4-msgpack php7.4-igbinary php7.4-ldap \ + php7.4-redis php7.4-xdebug \ + && php -r "readfile('https://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer \ + && curl -sL https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - \ + && apt-get install -y nodejs \ + && npm install -g npm \ + && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ + && apt-get update \ + && apt-get install -y yarn \ + && apt-get install -y mysql-client \ + && apt-get install -y postgresql-client \ + && apt-get -y autoremove \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +RUN setcap "cap_net_bind_service=+ep" /usr/bin/php7.4 + +RUN groupadd --force -g $WWWGROUP sail +RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail + +COPY start-container /usr/local/bin/start-container +COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY php.ini /etc/php/7.4/cli/conf.d/99-sail.ini +RUN chmod +x /usr/local/bin/start-container + +EXPOSE 8000 + +ENTRYPOINT ["start-container"] diff --git a/docker/7.4/php.ini b/docker/7.4/php.ini new file mode 100644 index 0000000..66d04d5 --- /dev/null +++ b/docker/7.4/php.ini @@ -0,0 +1,4 @@ +[PHP] +post_max_size = 100M +upload_max_filesize = 100M +variables_order = EGPCS diff --git a/docker/7.4/start-container b/docker/7.4/start-container new file mode 100644 index 0000000..b99ddd0 --- /dev/null +++ b/docker/7.4/start-container @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +if [ ! -z "$WWWUSER" ]; then + usermod -u $WWWUSER sail +fi + +if [ ! -d /.composer ]; then + mkdir /.composer +fi + +chmod -R ugo+rw /.composer + +if [ $# -gt 0 ]; then + exec gosu $WWWUSER "$@" +else + /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf +fi diff --git a/docker/7.4/supervisord.conf b/docker/7.4/supervisord.conf new file mode 100644 index 0000000..9d28479 --- /dev/null +++ b/docker/7.4/supervisord.conf @@ -0,0 +1,14 @@ +[supervisord] +nodaemon=true +user=root +logfile=/var/log/supervisor/supervisord.log +pidfile=/var/run/supervisord.pid + +[program:php] +command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80 +user=sail +environment=LARAVEL_SAIL="1" +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 diff --git a/docker/8.0/Dockerfile b/docker/8.0/Dockerfile new file mode 100644 index 0000000..152a0b6 --- /dev/null +++ b/docker/8.0/Dockerfile @@ -0,0 +1,60 @@ +FROM ubuntu:21.10 + +LABEL maintainer="Taylor Otwell" + +ARG WWWGROUP +ARG NODE_VERSION=16 + +WORKDIR /var/www/html + +ENV DEBIAN_FRONTEND noninteractive +ENV TZ=UTC + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN apt-get update \ + && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python2 \ + && mkdir -p ~/.gnupg \ + && chmod 600 ~/.gnupg \ + && echo "disable-ipv6" >> ~/.gnupg/dirmngr.conf \ + && apt-key adv --homedir ~/.gnupg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys E5267A6C \ + && apt-key adv --homedir ~/.gnupg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C300EE8C \ + && echo "deb http://ppa.launchpad.net/ondrej/php/ubuntu impish main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \ + && apt-get update \ + && apt-get install -y php8.0-cli php8.0-dev \ + php8.0-pgsql php8.0-sqlite3 php8.0-gd \ + php8.0-curl php8.0-memcached \ + php8.0-imap php8.0-mysql php8.0-mbstring \ + php8.0-xml php8.0-zip php8.0-bcmath php8.0-soap \ + php8.0-intl php8.0-readline php8.0-pcov \ + php8.0-msgpack php8.0-igbinary php8.0-ldap \ + php8.0-redis php8.0-swoole php8.0-xdebug \ + && php -r "readfile('https://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer \ + && curl -sL https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - \ + && apt-get install -y nodejs \ + && npm install -g npm \ + && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ + && apt-get update \ + && apt-get install -y yarn \ + && apt-get install -y mysql-client \ + && apt-get install -y postgresql-client \ + && apt-get -y autoremove \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +RUN update-alternatives --set php /usr/bin/php8.0 + +RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.0 + +RUN groupadd --force -g $WWWGROUP sail +RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail + +COPY start-container /usr/local/bin/start-container +COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY php.ini /etc/php/8.0/cli/conf.d/99-sail.ini +RUN chmod +x /usr/local/bin/start-container + +EXPOSE 8000 + +ENTRYPOINT ["start-container"] diff --git a/docker/8.0/php.ini b/docker/8.0/php.ini new file mode 100644 index 0000000..66d04d5 --- /dev/null +++ b/docker/8.0/php.ini @@ -0,0 +1,4 @@ +[PHP] +post_max_size = 100M +upload_max_filesize = 100M +variables_order = EGPCS diff --git a/docker/8.0/start-container b/docker/8.0/start-container new file mode 100644 index 0000000..b99ddd0 --- /dev/null +++ b/docker/8.0/start-container @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +if [ ! -z "$WWWUSER" ]; then + usermod -u $WWWUSER sail +fi + +if [ ! -d /.composer ]; then + mkdir /.composer +fi + +chmod -R ugo+rw /.composer + +if [ $# -gt 0 ]; then + exec gosu $WWWUSER "$@" +else + /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf +fi diff --git a/docker/8.0/supervisord.conf b/docker/8.0/supervisord.conf new file mode 100644 index 0000000..9d28479 --- /dev/null +++ b/docker/8.0/supervisord.conf @@ -0,0 +1,14 @@ +[supervisord] +nodaemon=true +user=root +logfile=/var/log/supervisor/supervisord.log +pidfile=/var/run/supervisord.pid + +[program:php] +command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80 +user=sail +environment=LARAVEL_SAIL="1" +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 diff --git a/docker/8.1/Dockerfile b/docker/8.1/Dockerfile new file mode 100644 index 0000000..a29bd44 --- /dev/null +++ b/docker/8.1/Dockerfile @@ -0,0 +1,59 @@ +FROM ubuntu:21.10 + +LABEL maintainer="Taylor Otwell" + +ARG WWWGROUP +ARG NODE_VERSION=16 + +WORKDIR /var/www/html + +ENV DEBIAN_FRONTEND noninteractive +ENV TZ=UTC + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN apt-get update \ + && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python2 \ + && mkdir -p ~/.gnupg \ + && chmod 600 ~/.gnupg \ + && echo "disable-ipv6" >> ~/.gnupg/dirmngr.conf \ + && apt-key adv --homedir ~/.gnupg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys E5267A6C \ + && apt-key adv --homedir ~/.gnupg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C300EE8C \ + && echo "deb http://ppa.launchpad.net/ondrej/php/ubuntu impish main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \ + && apt-get update \ + && apt-get install -y php8.1-cli php8.1-dev \ + php8.1-pgsql php8.1-sqlite3 php8.1-gd \ + php8.1-curl \ + php8.1-imap php8.1-mysql php8.1-mbstring \ + php8.1-xml php8.1-zip php8.1-bcmath php8.1-soap \ + php8.1-intl php8.1-readline \ + php8.1-ldap \ + php8.1-msgpack php8.1-igbinary php8.1-redis php8.1-swoole \ + php8.1-memcached php8.1-pcov php8.1-xdebug \ + && php -r "readfile('https://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer \ + && curl -sL https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - \ + && apt-get install -y nodejs \ + && npm install -g npm \ + && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ + && apt-get update \ + && apt-get install -y yarn \ + && apt-get install -y mysql-client \ + && apt-get install -y postgresql-client \ + && apt-get -y autoremove \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.1 + +RUN groupadd --force -g $WWWGROUP sail +RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail + +COPY start-container /usr/local/bin/start-container +COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY php.ini /etc/php/8.1/cli/conf.d/99-sail.ini +RUN chmod +x /usr/local/bin/start-container + +EXPOSE 8000 + +ENTRYPOINT ["start-container"] diff --git a/docker/8.1/php.ini b/docker/8.1/php.ini new file mode 100644 index 0000000..66d04d5 --- /dev/null +++ b/docker/8.1/php.ini @@ -0,0 +1,4 @@ +[PHP] +post_max_size = 100M +upload_max_filesize = 100M +variables_order = EGPCS diff --git a/docker/8.1/start-container b/docker/8.1/start-container new file mode 100644 index 0000000..b99ddd0 --- /dev/null +++ b/docker/8.1/start-container @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +if [ ! -z "$WWWUSER" ]; then + usermod -u $WWWUSER sail +fi + +if [ ! -d /.composer ]; then + mkdir /.composer +fi + +chmod -R ugo+rw /.composer + +if [ $# -gt 0 ]; then + exec gosu $WWWUSER "$@" +else + /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf +fi diff --git a/docker/8.1/supervisord.conf b/docker/8.1/supervisord.conf new file mode 100644 index 0000000..9d28479 --- /dev/null +++ b/docker/8.1/supervisord.conf @@ -0,0 +1,14 @@ +[supervisord] +nodaemon=true +user=root +logfile=/var/log/supervisor/supervisord.log +pidfile=/var/run/supervisord.pid + +[program:php] +command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80 +user=sail +environment=LARAVEL_SAIL="1" +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 diff --git a/docker/sail b/docker/sail new file mode 100644 index 0000000..c721d8a --- /dev/null +++ b/docker/sail @@ -0,0 +1,360 @@ +#!/usr/bin/env bash + +if ! [ -x "$(command -v docker-compose)" ]; then + shopt -s expand_aliases + alias docker-compose='docker compose' +fi + +UNAMEOUT="$(uname -s)" + +WHITE='\033[1;37m' +NC='\033[0m' + +# 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 + +# Source the ".env" file so Laravel's environment variables are available... +if [ -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_FILE=${SAIL_FILE:-"docker-compose.yml"} +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:-""} + +# Function that outputs Sail is not running... +function sail_is_not_running { + echo -e "${WHITE}Sail is not running.${NC}" >&2 + echo "" >&2 + echo -e "${WHITE}You may Sail using the following commands:${NC} './vendor/bin/sail up' or './vendor/bin/sail up -d'" >&2 + + exit 1 +} + +if [ -z "$SAIL_SKIP_CHECKS" ]; then + # Ensure that Docker is running... + if ! docker info > /dev/null 2>&1; then + echo -e "${WHITE}Docker is not running.${NC}" >&2 + + exit 1 + fi + + # Determine if Sail is currently up... + PSRESULT="$(docker-compose -f "$SAIL_FILE" ps -q)" + if docker-compose -f "$SAIL_FILE" ps "$APP_SERVICE" | grep 'Exit\|exited'; then + echo -e "${WHITE}Shutting down old Sail processes...${NC}" >&2 + + docker-compose -f "$SAIL_FILE" down > /dev/null 2>&1 + + EXEC="no" + elif [ -n "$PSRESULT" ]; then + EXEC="yes" + else + EXEC="no" + fi +else + EXEC="yes" +fi + +if [ $# -gt 0 ]; then + # Proxy PHP commands to the "php" binary on the application container... + if [ "$1" == "php" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + docker-compose -f $SAIL_FILE exec \ + -u sail \ + "$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 + docker-compose -f $SAIL_FILE exec \ + -u sail \ + "$APP_SERVICE" \ + ./vendor/bin/"$@" + 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 + docker-compose -f $SAIL_FILE exec \ + -u sail \ + "$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" ]; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + docker-compose -f $SAIL_FILE exec \ + -u sail \ + "$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 + docker-compose -f $SAIL_FILE exec \ + -u sail \ + -e XDEBUG_SESSION=1 \ + "$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 + docker-compose -f $SAIL_FILE exec \ + -u sail \ + "$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 + docker-compose -f $SAIL_FILE exec \ + -u sail \ + "$APP_SERVICE" \ + php vendor/bin/phpunit "$@" + 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 + docker-compose -f $SAIL_FILE exec \ + -u sail \ + -e "APP_URL=http://${APP_SERVICE}" \ + -e "DUSK_DRIVER_URL=http://selenium:4444/wd/hub" \ + "$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 + docker-compose -f $SAIL_FILE exec \ + -u sail \ + -e "APP_URL=http://${APP_SERVICE}" \ + -e "DUSK_DRIVER_URL=http://selenium:4444/wd/hub" \ + "$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 + docker-compose -f $SAIL_FILE exec \ + -u sail \ + "$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 + docker-compose -f $SAIL_FILE exec \ + -u sail \ + "$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 + docker-compose -f $SAIL_FILE exec \ + -u sail \ + "$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 + docker-compose -f $SAIL_FILE exec \ + -u sail \ + "$APP_SERVICE" \ + npx "$@" + 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 + docker-compose -f $SAIL_FILE exec \ + -u sail \ + "$APP_SERVICE" \ + yarn "$@" + 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 + docker-compose -f $SAIL_FILE exec \ + mysql \ + bash -c '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 + docker-compose -f $SAIL_FILE exec \ + mariadb \ + bash -c 'MYSQL_PWD=${MYSQL_PASSWORD} mysql -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 + docker-compose -f $SAIL_FILE exec \ + pgsql \ + bash -c '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 + docker-compose -f $SAIL_FILE exec \ + -u sail \ + "$APP_SERVICE" \ + bash "$@" + else + sail_is_not_running + fi + + # Initiate a root user Bash shell within the application container... + elif [ "$1" == "root-shell" ] ; then + shift 1 + + if [ "$EXEC" == "yes" ]; then + docker-compose -f $SAIL_FILE exec \ + "$APP_SERVICE" \ + bash "$@" + 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 + docker-compose -f $SAIL_FILE exec \ + 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 -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" \ + --subdomain="$SAIL_SHARE_SUBDOMAIN" \ + "$@" + else + sail_is_not_running + fi + + # Pass unknown commands to the "docker-compose" binary... + else + docker-compose -f $SAIL_FILE "$@" + fi +else + docker-compose -f $SAIL_FILE ps +fi diff --git a/sail b/sail new file mode 100755 index 0000000..f09f3c1 --- /dev/null +++ b/sail @@ -0,0 +1,468 @@ +#!/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}Yarn Commands:${NC}" + echo " ${GREEN}sail yarn ...${NC} Run a Yarn command" + echo " ${GREEN}sail yarn 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 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 dusk${NC} Run the 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 + 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... +if [ -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:-""} + +# 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 [ -x "$(command -v docker-compose)" ]; then + DOCKER_COMPOSE=(docker-compose) +else + DOCKER_COMPOSE=(docker compose) +fi + +if [ -n "$SAIL_FILES" ]; then + # Convert SAIL_FILES to an array... + 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 + ARGS+=(exec -u sail) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" ./vendor/bin/"$@") + 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" ]; 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_SESSION=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 "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 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 + +# 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} mysql -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) + [ ! -t 0 ] && ARGS+=(-T) + ARGS+=("$APP_SERVICE" bash "$@") + 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 -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" \ + --subdomain="$SAIL_SHARE_SUBDOMAIN" \ + "$@" + + exit + else + sail_is_not_running + fi + +# Pass unknown commands to the "docker-compose" binary... +else + ARGS+=("$@") +fi + +# Run Docker Compose with the defined arguments... +"${DOCKER_COMPOSE[@]}" "${ARGS[@]}"